Merge pull request 'feature/algorithm' (#5) from feature/algorithm into main
Reviewed-on: #5
This commit is contained in:
		
						commit
						0abde5611b
					
				
					 43 changed files with 3450 additions and 18685 deletions
				
			
		
							
								
								
									
										31
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										31
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,20 +1,41 @@ | ||||||
| /.idea | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||||||
|  | 
 | ||||||
| # dependencies | # dependencies | ||||||
| /node_modules | /node_modules | ||||||
|  | /.pnp | ||||||
|  | .pnp.* | ||||||
|  | .yarn/* | ||||||
|  | !.yarn/patches | ||||||
|  | !.yarn/plugins | ||||||
|  | !.yarn/releases | ||||||
|  | !.yarn/versions | ||||||
| 
 | 
 | ||||||
| # testing | # testing | ||||||
| /coverage | /coverage | ||||||
| 
 | 
 | ||||||
|  | # next.js | ||||||
|  | /.next/ | ||||||
|  | /out/ | ||||||
|  | 
 | ||||||
| # production | # production | ||||||
| /build | /build | ||||||
| 
 | 
 | ||||||
| # misc | # misc | ||||||
| .DS_Store | .DS_Store | ||||||
| .env.local | *.pem | ||||||
| .env.development.local |  | ||||||
| .env.test.local |  | ||||||
| .env.production.local |  | ||||||
| 
 | 
 | ||||||
|  | # debug | ||||||
| npm-debug.log* | npm-debug.log* | ||||||
| yarn-debug.log* | yarn-debug.log* | ||||||
| yarn-error.log* | yarn-error.log* | ||||||
|  | .pnpm-debug.log* | ||||||
|  | 
 | ||||||
|  | # env files (can opt-in for committing if needed) | ||||||
|  | .env* | ||||||
|  | 
 | ||||||
|  | # vercel | ||||||
|  | .vercel | ||||||
|  | 
 | ||||||
|  | # typescript | ||||||
|  | *.tsbuildinfo | ||||||
|  | ../next-env.d.ts | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								eslint.config.mjs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								eslint.config.mjs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | ||||||
|  | import { dirname } from "path"; | ||||||
|  | import { fileURLToPath } from "url"; | ||||||
|  | import { FlatCompat } from "@eslint/eslintrc"; | ||||||
|  | 
 | ||||||
|  | const __filename = fileURLToPath(import.meta.url); | ||||||
|  | const __dirname = dirname(__filename); | ||||||
|  | 
 | ||||||
|  | const compat = new FlatCompat({ | ||||||
|  |   baseDirectory: __dirname, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const eslintConfig = [ | ||||||
|  |   ...compat.extends("next/core-web-vitals", "next/typescript"), | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | export default eslintConfig; | ||||||
							
								
								
									
										5
									
								
								next-env.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								next-env.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | /// <reference types="next" />
 | ||||||
|  | /// <reference types="next/image-types/global" />
 | ||||||
|  | 
 | ||||||
|  | // NOTE: This file should not be edited
 | ||||||
|  | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
 | ||||||
							
								
								
									
										9
									
								
								next.config.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								next.config.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | import type {NextConfig} from "next"; | ||||||
|  | 
 | ||||||
|  | const nextConfig: NextConfig = { | ||||||
|  |     /* config options here */ | ||||||
|  |     output: "export", | ||||||
|  |     assetPrefix: "./" | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default nextConfig; | ||||||
							
								
								
									
										19044
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										19044
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										50
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										50
									
								
								package.json
									
										
									
									
									
								
							|  | @ -1,37 +1,25 @@ | ||||||
| { | { | ||||||
|   "dependencies": { |  | ||||||
|     "@types/jest": "^29.5.0", |  | ||||||
|     "@types/node": "^18.15.11", |  | ||||||
|     "@types/react": "^18.0.35", |  | ||||||
|     "@types/react-dom": "^18.0.11", |  | ||||||
|     "react": "^18.0.0", |  | ||||||
|     "react-dom": "^18.0.0", |  | ||||||
|     "typescript": "^5.0.4" |  | ||||||
|   }, |  | ||||||
|   "main": "/index.js", |  | ||||||
|   "homepage": ".", |  | ||||||
|   "devDependencies": { |  | ||||||
|     "react-scripts": "^5.0.1" |  | ||||||
|   }, |  | ||||||
|   "name": "a-maze-r", |   "name": "a-maze-r", | ||||||
|   "description": "The A-Maze-Ing Generator!", |   "version": "0.1.0", | ||||||
|   "version": "0.0.0", |   "private": true, | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "react-scripts start", |     "dev": "next dev", | ||||||
|     "build": "react-scripts build", |     "build": "next build", | ||||||
|     "test": "react-scripts test --env=jsdom", |     "start": "next start", | ||||||
|     "eject": "react-scripts eject" |     "lint": "next lint" | ||||||
|   }, |   }, | ||||||
|   "browserslist": { |   "dependencies": { | ||||||
|     "production": [ |     "react": "^19.0.0", | ||||||
|       ">0.2%", |     "react-dom": "^19.0.0", | ||||||
|       "not dead", |     "next": "15.1.2" | ||||||
|       "not op_mini all" |   }, | ||||||
|     ], |   "devDependencies": { | ||||||
|     "development": [ |     "typescript": "^5", | ||||||
|       "last 1 chrome version", |     "@types/node": "^20", | ||||||
|       "last 1 firefox version", |     "@types/react": "^19", | ||||||
|       "last 1 safari version" |     "@types/react-dom": "^19", | ||||||
|     ] |     "eslint": "^9", | ||||||
|  |     "eslint-config-next": "15.1.2", | ||||||
|  |     "@eslint/eslintrc": "^3" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 24 KiB | 
|  | @ -1,38 +0,0 @@ | ||||||
| <!doctype html> |  | ||||||
| <html lang="en"> |  | ||||||
| <head> |  | ||||||
|   <meta charset="utf-8"> |  | ||||||
|   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |  | ||||||
|   <meta name="theme-color" content="#000000"> |  | ||||||
|   <!-- |  | ||||||
|     manifest.json provides metadata used when your web app is added to the |  | ||||||
|     homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ |  | ||||||
|   --> |  | ||||||
|   <link rel="manifest" href="%PUBLIC_URL%/manifest.json"> |  | ||||||
|   <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> |  | ||||||
|   <!-- |  | ||||||
|     Notice the use of %PUBLIC_URL% in the tags above. |  | ||||||
|     It will be replaced with the URL of the `public` folder during the build. |  | ||||||
|     Only files inside the `public` folder can be referenced from the HTML. |  | ||||||
| 
 |  | ||||||
|     Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |  | ||||||
|     work correctly both with client-side routing and a non-root public URL. |  | ||||||
|     Learn how to configure a non-root public URL by running `npm run build`. |  | ||||||
|   --> |  | ||||||
|   <title>A-Maze-R</title> |  | ||||||
|    |  | ||||||
| </head> |  | ||||||
| <body> |  | ||||||
|   <div id="root"></div> |  | ||||||
|   <!-- |  | ||||||
|     This HTML file is a template. |  | ||||||
|     If you open it directly in the browser, you will see an empty page. |  | ||||||
| 
 |  | ||||||
|     You can add webfonts, meta tags, or analytics to this file. |  | ||||||
|     The build step will place the bundled scripts into the <body> tag. |  | ||||||
| 
 |  | ||||||
|     To begin the development, run `npm start` or `yarn start`. |  | ||||||
|     To create a production bundle, use `npm run build` or `yarn build`. |  | ||||||
|   --> |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
|  | @ -1,15 +0,0 @@ | ||||||
| { |  | ||||||
|   "short_name": "React App", |  | ||||||
|   "name": "Create React App Sample", |  | ||||||
|   "icons": [ |  | ||||||
|     { |  | ||||||
|       "src": "favicon.ico", |  | ||||||
|       "sizes": "192x192", |  | ||||||
|       "type": "image/png" |  | ||||||
|     } |  | ||||||
|   ], |  | ||||||
|   "start_url": "./index.html", |  | ||||||
|   "display": "standalone", |  | ||||||
|   "theme_color": "#000000", |  | ||||||
|   "background_color": "#ffffff" |  | ||||||
| } |  | ||||||
							
								
								
									
										32
									
								
								src/Cell.tsx
									
										
									
									
									
								
							
							
						
						
									
										32
									
								
								src/Cell.tsx
									
										
									
									
									
								
							|  | @ -1,32 +0,0 @@ | ||||||
| import {MazeCell} from "./model/Maze"; |  | ||||||
| import Coordinates from "./model/Coordinates"; |  | ||||||
| import {actionClickedCell} from "./state/action.ts"; |  | ||||||
| 
 |  | ||||||
| function isMarked(x: number, y: number, marked: Coordinates[]): boolean { |  | ||||||
|     return !!marked.find(e => e.x === x && e.y === y); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export default function Cell({x, y, state, dispatch}) { |  | ||||||
|     const cell: MazeCell = state.maze.grid[y][x]; |  | ||||||
|     let classes = "cell r" + y + " c" + x; |  | ||||||
|     if (cell.top) classes += " top"; |  | ||||||
|     if (cell.right) classes += " right"; |  | ||||||
|     if (cell.bottom) classes += " bottom"; |  | ||||||
|     if (cell.left) classes += " left"; |  | ||||||
|     if (cell.solution && state.showSolution) classes += " solution"; |  | ||||||
|     const marked = isMarked(x, y, state.userPath); |  | ||||||
|     if (marked) classes += " user"; |  | ||||||
|     return ( |  | ||||||
|         <div className={classes} |  | ||||||
|              onMouseEnter={(e) => { |  | ||||||
|                  const leftPressed = e.buttons & 0x1; |  | ||||||
|                  if (leftPressed) { |  | ||||||
|                      dispatch(actionClickedCell(x, y)); |  | ||||||
|                  } |  | ||||||
|              }} |  | ||||||
|              onClick={(e) => { |  | ||||||
|                  dispatch(actionClickedCell(x, y)); |  | ||||||
|              }}> |  | ||||||
|         </div> |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
|  | @ -1,96 +0,0 @@ | ||||||
| import {useState} from 'react'; |  | ||||||
| import ValidatingInputNumberField from "./ValidatingInputNumberField.tsx"; |  | ||||||
| import {actionLoadedMaze, actionLoadingFailed, actionStartedLoading} from "./state/action.ts"; |  | ||||||
| 
 |  | ||||||
| export default function InputForm({state, dispatch}) { |  | ||||||
|     const [width, setWidth] = useState(10); |  | ||||||
|     const [height, setHeight] = useState(10); |  | ||||||
|     const [id, setId] = useState(null as number); |  | ||||||
| 
 |  | ||||||
|     const handleSubmit = (e) => { |  | ||||||
|         e.preventDefault(); |  | ||||||
|         dispatch(actionStartedLoading()); |  | ||||||
|         const url = `https://manuel.friedli.info/labyrinth/create/json?w=${width}&h=${height}&id=${id || ''}`; |  | ||||||
|         fetch(url) |  | ||||||
|             .then(response => response.json()) |  | ||||||
|             // .then(result => new Promise(resolve => setTimeout(resolve, 600, result)))
 |  | ||||||
|             .then(result => { |  | ||||||
|                 dispatch(actionLoadedMaze(result)); |  | ||||||
|             }) |  | ||||||
|             .catch(reason => { |  | ||||||
|                 console.error("Failed to fetch maze data.", reason); |  | ||||||
|                 dispatch(actionLoadingFailed(reason)); |  | ||||||
|             }); |  | ||||||
|     }; |  | ||||||
|     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"} |  | ||||||
|                                         value={width} |  | ||||||
|                                         constraints={{ |  | ||||||
|                                             min: 2 |  | ||||||
|                                         }} |  | ||||||
|                                         validatorFn={validateWidthHeightInput} |  | ||||||
|                                         disabled={state.loading} |  | ||||||
|                                         onChange={setWidth} |  | ||||||
|             /><br/> |  | ||||||
|             <ValidatingInputNumberField id={"height"} |  | ||||||
|                                         label={"Height"} |  | ||||||
|                                         value={height} |  | ||||||
|                                         constraints={{ |  | ||||||
|                                             min: 2 |  | ||||||
|                                         }} |  | ||||||
|                                         validatorFn={validateWidthHeightInput} |  | ||||||
|                                         disabled={state.loading} |  | ||||||
|                                         onChange={setHeight} |  | ||||||
|             /><br/> |  | ||||||
|             <ValidatingInputNumberField id={"id"} |  | ||||||
|                                         label={"ID (optional)"} |  | ||||||
|                                         value={id} |  | ||||||
|                                         validatorFn={validateIdInput} |  | ||||||
|                                         disabled={state.loading} |  | ||||||
|                                         onChange={setId} |  | ||||||
|             /><br/> |  | ||||||
|             <button type={"submit"} |  | ||||||
|                     disabled={state.loading |  | ||||||
|                         || isNaN(width) |  | ||||||
|                         || isNaN(height) |  | ||||||
|                     }>{state.loading ? "Loading ..." : "Create Maze!"} |  | ||||||
|             </button> |  | ||||||
|         </form> |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
							
								
								
									
										22
									
								
								src/Maze.tsx
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								src/Maze.tsx
									
										
									
									
									
								
							|  | @ -1,22 +0,0 @@ | ||||||
| import Cell from "./Cell.tsx"; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| export default function Maze({state, dispatch}) { |  | ||||||
|     if (!state.maze) { |  | ||||||
|         return <div>No valid maze.</div> |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let maze: JSX.Element[] = []; |  | ||||||
|     for (let y = 0; y < state.maze.height; y++) { |  | ||||||
|         let row: JSX.Element[] = []; |  | ||||||
|         for (let x = 0; x < state.maze.width; x++) { |  | ||||||
|             row.push(<Cell key={`${x}x${y}`} x={x} y={y} state={state} dispatch={dispatch}/>) |  | ||||||
|         } |  | ||||||
|         maze.push(<div key={`r${y}`} className={"row"}>{row}</div>); |  | ||||||
|     } |  | ||||||
|     return ( |  | ||||||
|         <div className={"maze"}> |  | ||||||
|             {maze} |  | ||||||
|         </div> |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
|  | @ -1,15 +0,0 @@ | ||||||
| import {actionClosedMessageBanner} from "./state/action.ts"; |  | ||||||
| 
 |  | ||||||
| export default function MessageBanner({state, dispatch}) { |  | ||||||
|     function handleClose() { |  | ||||||
|         dispatch(actionClosedMessageBanner()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!!state.errorMessage) { |  | ||||||
|         return (<div className={"message-banner"}> |  | ||||||
|             {state.errorMessage} |  | ||||||
|             <button onClick={handleClose}>Dismiss message</button> |  | ||||||
|         </div>); |  | ||||||
|     } |  | ||||||
|     return <></>; |  | ||||||
| } |  | ||||||
|  | @ -1,41 +0,0 @@ | ||||||
| import React, {useState} from 'react'; |  | ||||||
| 
 |  | ||||||
| export default function ValidatingInputNumberField({ |  | ||||||
|                                                        id, |  | ||||||
|                                                        label, |  | ||||||
|                                                        value = 0, |  | ||||||
|                                                        constraints = {}, |  | ||||||
|                                                        validatorFn = (value) => { |  | ||||||
|                                                            return {valid: true, value}; |  | ||||||
|                                                        }, |  | ||||||
|                                                        disabled = false, |  | ||||||
|                                                        onChange = _ => { |  | ||||||
|                                                        } |  | ||||||
|                                                    }) { |  | ||||||
|     const [error, setError] = useState(null); |  | ||||||
| 
 |  | ||||||
|     const handleValueChange = (e) => { |  | ||||||
|         const value = e.target.value; |  | ||||||
|         const validation = validatorFn(value); |  | ||||||
|         if (!validation.valid) { |  | ||||||
|             setError(validation.message); |  | ||||||
|         } else { |  | ||||||
|             setError(null); |  | ||||||
|         } |  | ||||||
|         onChange(validation.value); |  | ||||||
|     }; |  | ||||||
|     return ( |  | ||||||
|         <> |  | ||||||
|             <label htmlFor={id}>{label}: </label> |  | ||||||
|             <input id={id} |  | ||||||
|                    type={"number"} |  | ||||||
|                    onChange={handleValueChange} |  | ||||||
|                    value={value || ""} |  | ||||||
|                    min={constraints.min || null} |  | ||||||
|                    max={constraints.max || null} |  | ||||||
|                    disabled={disabled} |  | ||||||
|             /> |  | ||||||
|             {!!error && <span>{error}</span>} |  | ||||||
|         </> |  | ||||||
|     ); |  | ||||||
| } |  | ||||||
							
								
								
									
										35
									
								
								src/app/cell.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/app/cell.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,35 @@ | ||||||
|  | .solution { | ||||||
|  |     background-color: var(--color-maze-cell-solution); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .solution:hover { | ||||||
|  |     background-color: var(--color-maze-cell-solution-highlight); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .top { | ||||||
|  |     border-top-color: var(--color-maze-border); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .right { | ||||||
|  |     border-right-color: var(--color-maze-border); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .bottom { | ||||||
|  |     border-bottom-color: var(--color-maze-border); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .left { | ||||||
|  |     border-left-color: var(--color-maze-border); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .user{ | ||||||
|  |     background-color: var(--color-maze-cell-user); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .user:hover { | ||||||
|  |     background-color: var(--color-maze-cell-user-highlight); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .solution.user { | ||||||
|  |     background-color: #c8ff00; | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								src/app/cell.module.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/app/cell.module.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | .cell { | ||||||
|  |     display: table-cell; | ||||||
|  |     border: 1px solid transparent; | ||||||
|  |     height: 2em; | ||||||
|  |     width: 2em; | ||||||
|  |     padding: 0; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .cell:hover { | ||||||
|  |     background-color: var(--color-background-highlight); | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								src/app/cell.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/app/cell.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | ||||||
|  | import {MazeCell} from "./model/maze.ts"; | ||||||
|  | import Coordinates from "./model/coordinates.ts"; | ||||||
|  | import {Action, actionClickedCell} from "./state/action.ts"; | ||||||
|  | import styles from "./cell.module.css"; | ||||||
|  | import "./cell.css"; | ||||||
|  | import {State} from "./state/state.ts"; | ||||||
|  | import {ActionDispatch} from "react"; | ||||||
|  | 
 | ||||||
|  | function isMarked(x: number, y: number, marked: Coordinates[]): boolean { | ||||||
|  |     return !!marked.find(e => e.x === x && e.y === y); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default function Cell({x, y, state, dispatch}: | ||||||
|  |                              { | ||||||
|  |                                  x: number, | ||||||
|  |                                  y: number, | ||||||
|  |                                  state: State, | ||||||
|  |                                  dispatch: ActionDispatch<[Action]> | ||||||
|  |                              }) { | ||||||
|  |     const cell: MazeCell = state.maze!.grid[y][x]; | ||||||
|  |     let classes = " r" + y + " c" + x; | ||||||
|  |     if (cell.top) classes += " top"; | ||||||
|  |     if (cell.right) classes += " right"; | ||||||
|  |     if (cell.bottom) classes += " bottom"; | ||||||
|  |     if (cell.left) classes += " left"; | ||||||
|  |     if (cell.solution && state.showSolution) classes += " solution"; | ||||||
|  |     const marked = isMarked(x, y, state.userPath); | ||||||
|  |     if (marked) classes += " user"; | ||||||
|  |     return ( | ||||||
|  |         <div className={styles.cell + classes} | ||||||
|  |              onMouseEnter={(e) => { | ||||||
|  |                  const leftPressed = e.buttons & 0x1; | ||||||
|  |                  if (leftPressed) { | ||||||
|  |                      dispatch(actionClickedCell(x, y)); | ||||||
|  |                  } | ||||||
|  |              }} | ||||||
|  |              onClick={() => { | ||||||
|  |                  dispatch(actionClickedCell(x, y)); | ||||||
|  |              }}> | ||||||
|  |         </div> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/app/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/app/favicon.ico
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 894 B | 
							
								
								
									
										54
									
								
								src/app/globals.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/app/globals.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | ||||||
|  | :root { | ||||||
|  |     box-sizing: border-box; | ||||||
|  |     --color-background: #e8e6e3; | ||||||
|  |     --color-foreground: #181a1b; | ||||||
|  |     --color-form-error: #ff0000; | ||||||
|  |     --color-border: #5d6164; | ||||||
|  |     --color-background-highlight: #f8f7f4; | ||||||
|  |     --color-foreground-highlight: #292b2c; | ||||||
|  |     --color-border-highlight: #6e7275; | ||||||
|  |     --color-maze-background: #e8e6e3; | ||||||
|  |     --color-maze-border: #333333; | ||||||
|  |     --color-maze-cell-solution: #b1d5b1; | ||||||
|  |     --color-maze-cell-solution-highlight: #b9e8b9; | ||||||
|  |     --color-maze-cell-user: #ffcc00; | ||||||
|  |     --color-maze-cell-user-highlight: #ffdd22; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @media (prefers-color-scheme: dark) { | ||||||
|  |     :root { | ||||||
|  |         --color-background: #181a1b; | ||||||
|  |         --color-foreground: #e8e6e3; | ||||||
|  |         --color-form-error: #ff0000; | ||||||
|  |         --color-border: #5d6164; | ||||||
|  |         --color-background-highlight: #292b2c; | ||||||
|  |         --color-foreground-highlight: #f8f7f4; | ||||||
|  |         --color-border-highlight: #6e7275; | ||||||
|  |         --color-maze-background: #181a1b; | ||||||
|  |         --color-maze-border: #e8e6e3; | ||||||
|  |         --color-maze-cell-solution: #213d21; | ||||||
|  |         --color-maze-cell-solution-highlight: #3d6e3d; | ||||||
|  |         --color-maze-cell-user: #ffcc00; | ||||||
|  |         --color-maze-cell-user-highlight: #ffdd22; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | body { | ||||||
|  |     color: var(--color-foreground); | ||||||
|  |     background: var(--color-background); | ||||||
|  |     font-family: "Cantarell", Arial, Helvetica, sans-serif; | ||||||
|  |     -webkit-font-smoothing: antialiased; | ||||||
|  |     -moz-osx-font-smoothing: grayscale; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | input, select { | ||||||
|  |     background-color: var(--color-background); | ||||||
|  |     border: 1px solid var(--color-border); | ||||||
|  |     color: var(--color-foreground); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | input:hover, input:focus, select:hover, select:focus { | ||||||
|  |     background-color: var(--color-background-highlight); | ||||||
|  |     border-color: var(--color-border-highlight); | ||||||
|  |     color: var(--color-foreground-highlight); | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								src/app/input-form.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/app/input-form.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | ||||||
|  | form { | ||||||
|  |     display: flex; | ||||||
|  |     flex-flow: column; | ||||||
|  |     align-items: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | input, select { | ||||||
|  |     background-color: var(--color-background); | ||||||
|  |     border: 1px solid var(--color-border); | ||||||
|  |     color: var(--color-foreground); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | input:hover, input:focus, select:hover, select:focus { | ||||||
|  |     background-color: var(--color-background-highlight); | ||||||
|  |     border-color: var(--color-border-highlight); | ||||||
|  |     color: var(--color-foreground-highlight); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | input:invalid { | ||||||
|  |     border-color: var(--color-form-error); | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								src/app/input-form.module.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/app/input-form.module.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,14 @@ | ||||||
|  | .inputform { | ||||||
|  |     display: grid; | ||||||
|  |     grid-row-gap: 0.5em; | ||||||
|  |     grid-template-columns: 7em 14em auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .submitbutton { | ||||||
|  |     background-color: #fc0; | ||||||
|  |     border: 1px solid var(--color-foreground); | ||||||
|  |     padding: 0.5em; | ||||||
|  |     border-radius: 0.5em; | ||||||
|  |     margin-top: 2em; | ||||||
|  |     width: 11em; | ||||||
|  | } | ||||||
							
								
								
									
										118
									
								
								src/app/input-form.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/app/input-form.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,118 @@ | ||||||
|  | import {ActionDispatch, FormEvent, useState} from 'react'; | ||||||
|  | import ValidatingInputNumberField, {ValidatorFunction} from "./validating-input-number-field.tsx"; | ||||||
|  | import {Action, actionLoadedMaze, actionLoadingFailed, actionStartedLoading} from "./state/action.ts"; | ||||||
|  | import styles from "./input-form.module.css"; | ||||||
|  | import "./input-form.css"; | ||||||
|  | import {State} from "@/app/state/state.ts"; | ||||||
|  | 
 | ||||||
|  | export default function InputForm({state, dispatch}: { | ||||||
|  |     state: State, | ||||||
|  |     dispatch: ActionDispatch<[Action]> | ||||||
|  | }) { | ||||||
|  |     const [width, setWidth] = useState(10); | ||||||
|  |     const [height, setHeight] = useState(10); | ||||||
|  |     const [id, setId] = useState<number>(); | ||||||
|  |     const [algorithm, setAlgorithm] = useState('wilson'); | ||||||
|  | 
 | ||||||
|  |     const handleSubmit = (e: FormEvent) => { | ||||||
|  |         e.preventDefault(); | ||||||
|  |         dispatch(actionStartedLoading()); | ||||||
|  |         const url = `https://manuel.friedli.info/labyrinth/create/json?w=${width}&h=${height}&id=${id || ''}&algorithm=${algorithm}`; | ||||||
|  |         fetch(url) | ||||||
|  |             .then(response => response.json()) | ||||||
|  |             // .then(result => new Promise(resolve => setTimeout(resolve, 600, result)))
 | ||||||
|  |             .then(result => { | ||||||
|  |                 dispatch(actionLoadedMaze(result)); | ||||||
|  |             }) | ||||||
|  |             .catch(reason => { | ||||||
|  |                 console.error("Failed to fetch maze data.", reason); | ||||||
|  |                 dispatch(actionLoadingFailed(reason)); | ||||||
|  |             }); | ||||||
|  |     }; | ||||||
|  |     const validateWidthHeightInput: ValidatorFunction<string, number> = value => { | ||||||
|  |         const numberValue = Number(value); | ||||||
|  |         if (isNaN(numberValue) || "" === value || (Math.floor(numberValue) !== numberValue)) { | ||||||
|  |             return { | ||||||
|  |                 valid: false, | ||||||
|  |                 message: "Must be an integer greater than 1." | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |         if (numberValue < 1) { | ||||||
|  |             return { | ||||||
|  |                 valid: false, | ||||||
|  |                 message: "Must be greater than 1." | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |         return { | ||||||
|  |             valid: true, | ||||||
|  |             value: numberValue | ||||||
|  |         }; | ||||||
|  |     }; | ||||||
|  |     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").
 | ||||||
|  |         if (isNaN(numberValue) || Math.floor(numberValue) !== numberValue) { | ||||||
|  |             return { | ||||||
|  |                 valid: false, | ||||||
|  |                 message: "Must be empty or an integer" | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |         return { | ||||||
|  |             valid: true, | ||||||
|  |             value: numberValue | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     return ( | ||||||
|  |         <form onSubmit={handleSubmit}> | ||||||
|  |             <div className={styles.inputform}> | ||||||
|  |                 <ValidatingInputNumberField id={"width"} | ||||||
|  |                                             label={"Width"} | ||||||
|  |                                             value={width} | ||||||
|  |                                             constraints={{ | ||||||
|  |                                                 min: 2 | ||||||
|  |                                             }} | ||||||
|  |                                             validatorFn={validateWidthHeightInput} | ||||||
|  |                                             disabled={state.loading} | ||||||
|  |                                             onChange={setWidth} | ||||||
|  |                 /> | ||||||
|  |                 <ValidatingInputNumberField id={"height"} | ||||||
|  |                                             label={"Height"} | ||||||
|  |                                             value={height} | ||||||
|  |                                             constraints={{ | ||||||
|  |                                                 min: 2 | ||||||
|  |                                             }} | ||||||
|  |                                             validatorFn={validateWidthHeightInput} | ||||||
|  |                                             disabled={state.loading} | ||||||
|  |                                             onChange={setHeight} | ||||||
|  |                 /> | ||||||
|  |                 <ValidatingInputNumberField id={"id"} | ||||||
|  |                                             label={"ID (optional)"} | ||||||
|  |                                             value={id} | ||||||
|  |                                             validatorFn={validateIdInput} | ||||||
|  |                                             disabled={state.loading} | ||||||
|  |                                             onChange={setId} | ||||||
|  |                 /> | ||||||
|  |                 <label htmlFor="algorithm">Algorithm:</label> | ||||||
|  |                 <select id={"algorithm"} | ||||||
|  |                         value={algorithm} | ||||||
|  |                         disabled={state.loading} | ||||||
|  |                         onChange={e => setAlgorithm(e.target.value)}> | ||||||
|  |                     <option value="wilson">Wilson</option> | ||||||
|  |                     <option value="random">Random Depth First</option> | ||||||
|  |                 </select> | ||||||
|  |             </div> | ||||||
|  |             <button type={"submit"} | ||||||
|  |                     disabled={state.loading | ||||||
|  |                         || isNaN(width) | ||||||
|  |                         || isNaN(height) | ||||||
|  |                     } | ||||||
|  |                     className={styles.submitbutton}>{state.loading ? "Loading ..." : "Create Maze!"} | ||||||
|  |             </button> | ||||||
|  |         </form> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								src/app/layout.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/app/layout.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | import type {Metadata} from "next"; | ||||||
|  | import "./globals.css"; | ||||||
|  | 
 | ||||||
|  | export const metadata: Metadata = { | ||||||
|  |     title: "A-Maze-R! Create your own Maze!", | ||||||
|  |     description: "A Maze Generator by fritteli", | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default function RootLayout({children}: Readonly<{ | ||||||
|  |     children: React.ReactNode; | ||||||
|  | }>) { | ||||||
|  |     return ( | ||||||
|  |         <html lang="en"> | ||||||
|  |         <body> | ||||||
|  |         {children} | ||||||
|  |         </body> | ||||||
|  |         </html> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								src/app/maze.module.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/app/maze.module.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | ||||||
|  | .maze { | ||||||
|  |     display: table; | ||||||
|  |     border-collapse: collapse; | ||||||
|  |     background-color: var(--color-maze-background); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .row { | ||||||
|  |     display: table-row; | ||||||
|  |     margin: 0; | ||||||
|  |     padding: 0; | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								src/app/maze.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/app/maze.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | ||||||
|  | import Cell from "./cell.tsx"; | ||||||
|  | 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}: | ||||||
|  |                              { | ||||||
|  |                                  state: State, | ||||||
|  |                                  dispatch: ActionDispatch<[Action]> | ||||||
|  |                              }) { | ||||||
|  |     if (!state.maze) { | ||||||
|  |         return <div>No valid maze.</div> | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const maze: JSX.Element[] = []; | ||||||
|  |     for (let y = 0; y < state.maze.height; y++) { | ||||||
|  |         const row: JSX.Element[] = []; | ||||||
|  |         for (let x = 0; x < state.maze.width; x++) { | ||||||
|  |             row.push(<Cell key={`${x}x${y}`} x={x} y={y} state={state} dispatch={dispatch}/>) | ||||||
|  |         } | ||||||
|  |         maze.push(<div key={`r${y}`} className={styles.row}>{row}</div>); | ||||||
|  |     } | ||||||
|  |     return ( | ||||||
|  |         <div className={styles.maze}> | ||||||
|  |             {maze} | ||||||
|  |         </div> | ||||||
|  |     ); | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								src/app/message-banner.module.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/app/message-banner.module.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | ||||||
|  | .banner { | ||||||
|  |     height: 2em; | ||||||
|  | } | ||||||
|  | .empty { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .message { | ||||||
|  |     border: 1px solid red; | ||||||
|  |     background-color: #ff000044; | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								src/app/message-banner.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/app/message-banner.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | ||||||
|  | import {Action, actionClosedMessageBanner} from "./state/action.ts"; | ||||||
|  | import styles from "./message-banner.module.css"; | ||||||
|  | import {State} from "@/app/state/state.ts"; | ||||||
|  | import {ActionDispatch} from "react"; | ||||||
|  | 
 | ||||||
|  | export default function MessageBanner({state, dispatch}: | ||||||
|  |                                       { | ||||||
|  |                                           state: State; | ||||||
|  |                                           dispatch: ActionDispatch<[Action]> | ||||||
|  |                                       }) { | ||||||
|  |     function handleClose() { | ||||||
|  |         dispatch(actionClosedMessageBanner()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!!state.errorMessage) { | ||||||
|  |         return ( | ||||||
|  |             <div className={styles.banner + " " + styles.message}> | ||||||
|  |                 {state.errorMessage} | ||||||
|  |                 <button onClick={handleClose}>Dismiss message</button> | ||||||
|  |             </div> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     return (<div className={styles.banner + " " + styles.empty}></div>); | ||||||
|  | } | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| import Coordinates from "./Coordinates"; | import Coordinates from "./coordinates.ts"; | ||||||
| 
 | 
 | ||||||
| export default interface Maze { | export default interface Maze { | ||||||
|     id: string, |     id: string, | ||||||
|  | @ -6,6 +6,7 @@ export default interface Maze { | ||||||
|     height: number, |     height: number, | ||||||
|     start: Coordinates, |     start: Coordinates, | ||||||
|     end: Coordinates, |     end: Coordinates, | ||||||
|  |     algorithm: string, | ||||||
|     grid: MazeCell[][] |     grid: MazeCell[][] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
							
								
								
									
										179
									
								
								src/app/page.module.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/app/page.module.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,179 @@ | ||||||
|  | .page { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .page h1.mainheading, .page h2.mainheading { | ||||||
|  |     text-align: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .page h2.mainheading { | ||||||
|  |     font-size: medium; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /*.page {*/ | ||||||
|  | /*  --gray-rgb: 0, 0, 0;*/ | ||||||
|  | /*  --gray-alpha-200: rgba(var(--gray-rgb), 0.08);*/ | ||||||
|  | /*  --gray-alpha-100: rgba(var(--gray-rgb), 0.05);*/ | ||||||
|  | 
 | ||||||
|  | /*  --button-primary-hover: #383838;*/ | ||||||
|  | /*  --button-secondary-hover: #f2f2f2;*/ | ||||||
|  | 
 | ||||||
|  | /*  display: grid;*/ | ||||||
|  | /*  grid-template-rows: 20px 1fr 20px;*/ | ||||||
|  | /*  align-items: center;*/ | ||||||
|  | /*  justify-items: center;*/ | ||||||
|  | /*  min-height: 100svh;*/ | ||||||
|  | /*  padding: 80px;*/ | ||||||
|  | /*  gap: 64px;*/ | ||||||
|  | /*  font-family: var(--font-geist-sans);*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*@media (prefers-color-scheme: dark) {*/ | ||||||
|  | /*  .page {*/ | ||||||
|  | /*    --gray-rgb: 255, 255, 255;*/ | ||||||
|  | /*    --gray-alpha-200: rgba(var(--gray-rgb), 0.145);*/ | ||||||
|  | /*    --gray-alpha-100: rgba(var(--gray-rgb), 0.06);*/ | ||||||
|  | 
 | ||||||
|  | /*    --button-primary-hover: #ccc;*/ | ||||||
|  | /*    --button-secondary-hover: #1a1a1a;*/ | ||||||
|  | /*  }*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*.main {*/ | ||||||
|  | /*  display: flex;*/ | ||||||
|  | /*  flex-direction: column;*/ | ||||||
|  | /*  gap: 32px;*/ | ||||||
|  | /*  grid-row-start: 2;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*.main ol {*/ | ||||||
|  | /*  font-family: var(--font-geist-mono);*/ | ||||||
|  | /*  padding-left: 0;*/ | ||||||
|  | /*  margin: 0;*/ | ||||||
|  | /*  font-size: 14px;*/ | ||||||
|  | /*  line-height: 24px;*/ | ||||||
|  | /*  letter-spacing: -0.01em;*/ | ||||||
|  | /*  list-style-position: inside;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*.main li:not(:last-of-type) {*/ | ||||||
|  | /*  margin-bottom: 8px;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*.main code {*/ | ||||||
|  | /*  font-family: inherit;*/ | ||||||
|  | /*  background: var(--gray-alpha-100);*/ | ||||||
|  | /*  padding: 2px 4px;*/ | ||||||
|  | /*  border-radius: 4px;*/ | ||||||
|  | /*  font-weight: 600;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*.ctas {*/ | ||||||
|  | /*  display: flex;*/ | ||||||
|  | /*  gap: 16px;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*.ctas a {*/ | ||||||
|  | /*  appearance: none;*/ | ||||||
|  | /*  border-radius: 128px;*/ | ||||||
|  | /*  height: 48px;*/ | ||||||
|  | /*  padding: 0 20px;*/ | ||||||
|  | /*  border: none;*/ | ||||||
|  | /*  border: 1px solid transparent;*/ | ||||||
|  | /*  transition:*/ | ||||||
|  | /*    background 0.2s,*/ | ||||||
|  | /*    color 0.2s,*/ | ||||||
|  | /*    border-color 0.2s;*/ | ||||||
|  | /*  cursor: pointer;*/ | ||||||
|  | /*  display: flex;*/ | ||||||
|  | /*  align-items: center;*/ | ||||||
|  | /*  justify-content: center;*/ | ||||||
|  | /*  font-size: 16px;*/ | ||||||
|  | /*  line-height: 20px;*/ | ||||||
|  | /*  font-weight: 500;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*a.primary {*/ | ||||||
|  | /*  background: var(--foreground);*/ | ||||||
|  | /*  color: var(--background);*/ | ||||||
|  | /*  gap: 8px;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*a.secondary {*/ | ||||||
|  | /*  border-color: var(--gray-alpha-200);*/ | ||||||
|  | /*  min-width: 180px;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*.footer {*/ | ||||||
|  | /*  grid-row-start: 3;*/ | ||||||
|  | /*  display: flex;*/ | ||||||
|  | /*  gap: 24px;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*.footer a {*/ | ||||||
|  | /*  display: flex;*/ | ||||||
|  | /*  align-items: center;*/ | ||||||
|  | /*  gap: 8px;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*.footer img {*/ | ||||||
|  | /*  flex-shrink: 0;*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*!* Enable hover only on non-touch devices *!*/ | ||||||
|  | /*@media (hover: hover) and (pointer: fine) {*/ | ||||||
|  | /*  a.primary:hover {*/ | ||||||
|  | /*    background: var(--button-primary-hover);*/ | ||||||
|  | /*    border-color: transparent;*/ | ||||||
|  | /*  }*/ | ||||||
|  | 
 | ||||||
|  | /*  a.secondary:hover {*/ | ||||||
|  | /*    background: var(--button-secondary-hover);*/ | ||||||
|  | /*    border-color: transparent;*/ | ||||||
|  | /*  }*/ | ||||||
|  | 
 | ||||||
|  | /*  .footer a:hover {*/ | ||||||
|  | /*    text-decoration: underline;*/ | ||||||
|  | /*    text-underline-offset: 4px;*/ | ||||||
|  | /*  }*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*@media (max-width: 600px) {*/ | ||||||
|  | /*  .page {*/ | ||||||
|  | /*    padding: 32px;*/ | ||||||
|  | /*    padding-bottom: 80px;*/ | ||||||
|  | /*  }*/ | ||||||
|  | 
 | ||||||
|  | /*  .main {*/ | ||||||
|  | /*    align-items: center;*/ | ||||||
|  | /*  }*/ | ||||||
|  | 
 | ||||||
|  | /*  .main ol {*/ | ||||||
|  | /*    text-align: center;*/ | ||||||
|  | /*  }*/ | ||||||
|  | 
 | ||||||
|  | /*  .ctas {*/ | ||||||
|  | /*    flex-direction: column;*/ | ||||||
|  | /*  }*/ | ||||||
|  | 
 | ||||||
|  | /*  .ctas a {*/ | ||||||
|  | /*    font-size: 14px;*/ | ||||||
|  | /*    height: 40px;*/ | ||||||
|  | /*    padding: 0 16px;*/ | ||||||
|  | /*  }*/ | ||||||
|  | 
 | ||||||
|  | /*  a.secondary {*/ | ||||||
|  | /*    min-width: auto;*/ | ||||||
|  | /*  }*/ | ||||||
|  | 
 | ||||||
|  | /*  .footer {*/ | ||||||
|  | /*    flex-wrap: wrap;*/ | ||||||
|  | /*    align-items: center;*/ | ||||||
|  | /*    justify-content: center;*/ | ||||||
|  | /*  }*/ | ||||||
|  | /*}*/ | ||||||
|  | 
 | ||||||
|  | /*@media (prefers-color-scheme: dark) {*/ | ||||||
|  | /*  .logo {*/ | ||||||
|  | /*    filter: invert();*/ | ||||||
|  | /*  }*/ | ||||||
|  | /*}*/ | ||||||
|  | @ -1,23 +1,29 @@ | ||||||
| import {useReducer} from 'react'; | 'use client' | ||||||
| import Maze from "./Maze.tsx"; |  | ||||||
| import InputForm from "./InputForm.tsx"; |  | ||||||
| import reduce from "./state/reducer.ts"; |  | ||||||
| import MessageBanner from "./MessageBanner.tsx"; |  | ||||||
| import {INITIAL_STATE} from "./state/state.ts"; |  | ||||||
| import {actionToggledShowSolution} from "./state/action.ts"; |  | ||||||
| 
 | 
 | ||||||
| export default function App() { | import styles from "./page.module.css"; | ||||||
|  | import {useReducer} from "react"; | ||||||
|  | import reduce from "./state/reducer.ts"; | ||||||
|  | import {INITIAL_STATE} from "./state/state.ts"; | ||||||
|  | import MessageBanner from "./message-banner.tsx"; | ||||||
|  | import InputForm from "./input-form.tsx"; | ||||||
|  | import {actionToggledShowSolution} from "./state/action.ts"; | ||||||
|  | import Maze from "./maze.tsx"; | ||||||
|  | 
 | ||||||
|  | export default function Home() { | ||||||
|     const [state, dispatch] = useReducer(reduce, INITIAL_STATE); |     const [state, dispatch] = useReducer(reduce, INITIAL_STATE); | ||||||
|     const hasValidMaze = !!state.maze; |     const hasValidMaze = !!state.maze; | ||||||
|     return ( |     return ( | ||||||
|         <> |         <div className={styles.page}> | ||||||
|  |             <h1 className={styles.mainheading}>A-Maze-R! Create your own maze!</h1> | ||||||
|  |             <h2 className={styles.mainheading}>A fun little maze generator written by fritteli</h2> | ||||||
|             <MessageBanner state={state} |             <MessageBanner state={state} | ||||||
|                            dispatch={dispatch}/> |                            dispatch={dispatch}/> | ||||||
|             <InputForm state={state} |             <InputForm state={state} | ||||||
|                        dispatch={dispatch}/> |                        dispatch={dispatch}/> | ||||||
|             {hasValidMaze && |             {hasValidMaze && | ||||||
|                 <> |                 <> | ||||||
|                     <h1>The Maze ({state.maze.width}x{state.maze.height}, ID: {state.maze.id})</h1> |                     <h1>The Maze ({state.maze!.width}x{state.maze!.height}, Algorithm: {state.maze!.algorithm}, | ||||||
|  |                         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)); | ||||||
|  | @ -27,6 +33,6 @@ export default function App() { | ||||||
|                           dispatch={dispatch}/> |                           dispatch={dispatch}/> | ||||||
|                 </> |                 </> | ||||||
|             } |             } | ||||||
|         </> |         </div> | ||||||
|     ); |     ); | ||||||
| } | } | ||||||
|  | @ -1,9 +1,9 @@ | ||||||
| import Maze from "../model/Maze"; | 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'; | ||||||
|  | @ -9,8 +9,9 @@ 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) { | export default function reduce(state: State, action: Action): State { | ||||||
|     switch (action.type) { |     switch (action.type) { | ||||||
|         case ID_ACTION_STARTED_LOADING: { |         case ID_ACTION_STARTED_LOADING: { | ||||||
|             return { |             return { | ||||||
|  | @ -24,7 +25,7 @@ export default function reduce(state: State, action: Action) { | ||||||
|             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) { | ||||||
|         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) { | ||||||
|         } |         } | ||||||
|         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}`); | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import Coordinates from "../model/Coordinates"; | import Coordinates from "../model/coordinates.ts"; | ||||||
| import Maze from "../model/Maze"; | import Maze from "../model/maze.ts"; | ||||||
| 
 | 
 | ||||||
| export interface State { | export interface State { | ||||||
|     errorMessage: string | null, |     errorMessage: string | null, | ||||||
|  | @ -1,15 +1,16 @@ | ||||||
| import {State} from "./state"; | import {State} from "./state"; | ||||||
| import Coordinates from "../model/Coordinates"; | import Coordinates from "../model/coordinates.ts"; | ||||||
| import {MazeCell} from "../model/Maze"; | 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)) { | ||||||
|  |         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)) { | ||||||
|             // The clicked cell is not yet part of the userpath --> add it.
 |             // The clicked cell is not yet part of the userpath --> add it.
 | ||||||
|             // If it's the end tile, also show a congratulation message
 |             // If it's the end tile, also show a congratulation message
 | ||||||
|             const showMessage = x === state.maze.end.x && y === state.maze.end.y; |             const showMessage = x === maze.end.x && y === maze.end.y; | ||||||
|             return { |             return { | ||||||
|                 ...state, |                 ...state, | ||||||
|                 userPath: [...state.userPath, {x, y}], |                 userPath: [...state.userPath, {x, y}], | ||||||
|  | @ -17,7 +18,7 @@ export default function handleUserClicked(state: State, x: number, y: number): S | ||||||
|             }; |             }; | ||||||
|         } else { |         } else { | ||||||
|             // The clicked cell IS part of the userpath. Is it the last cell of it?
 |             // The clicked cell IS part of the userpath. Is it the last cell of it?
 | ||||||
|             const lastCoordsFromUserPath = getLastCoordsFromUserPath(state); |             const lastCoordsFromUserPath = getLastCoordsFromUserPath(state)!; | ||||||
|             if (lastCoordsFromUserPath.x === x && lastCoordsFromUserPath.y === y) { |             if (lastCoordsFromUserPath.x === x && lastCoordsFromUserPath.y === y) { | ||||||
|                 // Yes, it's the last cell of the userpath --> remove it.
 |                 // Yes, it's the last cell of the userpath --> remove it.
 | ||||||
|                 return { |                 return { | ||||||
|  | @ -36,7 +37,7 @@ function isClickAllowed(x: number, y: number, state: State): boolean { | ||||||
|     const lastCoordsFromUserPath = getLastCoordsFromUserPath(state); |     const lastCoordsFromUserPath = getLastCoordsFromUserPath(state); | ||||||
|     if (!lastCoordsFromUserPath) { |     if (!lastCoordsFromUserPath) { | ||||||
|         // when nothing has been marked yet, we can only toggle the starting position
 |         // when nothing has been marked yet, we can only toggle the starting position
 | ||||||
|         return x === state.maze.start.x && y === state.maze.start.y; |         return x === state.maze!.start.x && y === state.maze!.start.y; | ||||||
|     } |     } | ||||||
|     if (lastCoordsFromUserPath.x === x && lastCoordsFromUserPath.y === y) { |     if (lastCoordsFromUserPath.x === x && lastCoordsFromUserPath.y === y) { | ||||||
|         // toggling the last position in the path is always allowed
 |         // toggling the last position in the path is always allowed
 | ||||||
|  | @ -70,7 +71,7 @@ function isClickAllowed(x: number, y: number, state: State): boolean { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getCellAt(coords: Coordinates, state: State): MazeCell { | function getCellAt(coords: Coordinates, state: State): MazeCell { | ||||||
|     return state.maze.grid[coords.y][coords.x]; |     return state.maze!.grid[coords.y][coords.x]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getLastCoordsFromUserPath(state: State): Coordinates | null { | function getLastCoordsFromUserPath(state: State): Coordinates | null { | ||||||
							
								
								
									
										60
									
								
								src/app/validating-input-number-field.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/app/validating-input-number-field.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,60 @@ | ||||||
|  | import React, {ChangeEventHandler, useState} from 'react'; | ||||||
|  | 
 | ||||||
|  | export default function ValidatingInputNumberField({ | ||||||
|  |                                                        id, | ||||||
|  |                                                        label, | ||||||
|  |                                                        value = 0, | ||||||
|  |                                                        constraints = undefined, | ||||||
|  |                                                        validatorFn = (value) => { | ||||||
|  |                                                            return {valid: true, value: Number(value)}; | ||||||
|  |                                                        }, | ||||||
|  |                                                        disabled = false, | ||||||
|  |                                                        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<string>(); | ||||||
|  | 
 | ||||||
|  |     const handleValueChange: ChangeEventHandler<HTMLInputElement> = (e) => { | ||||||
|  |         const value = e.target.value; | ||||||
|  |         const validation = validatorFn(value); | ||||||
|  |         if (!validation.valid) { | ||||||
|  |             setError(validation.message); | ||||||
|  |         } else { | ||||||
|  |             setError(undefined); | ||||||
|  |         } | ||||||
|  |         if (validation.value) { | ||||||
|  |             onChange(validation.value); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     return ( | ||||||
|  |         <> | ||||||
|  |             <label htmlFor={id}>{label}: </label> | ||||||
|  |             <input id={id} | ||||||
|  |                    type={"number"} | ||||||
|  |                    onChange={handleValueChange} | ||||||
|  |                    value={value || ""} | ||||||
|  |                    min={constraints?.min} | ||||||
|  |                    max={constraints?.max} | ||||||
|  |                    disabled={disabled} | ||||||
|  |             /> | ||||||
|  |             <span>{error}</span> | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface Validation<T> { | ||||||
|  |     valid: boolean; | ||||||
|  |     message?: string; | ||||||
|  |     value?: T; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type ValidatorFunction<I, T> = (v: I) => Validation<T>; | ||||||
|  | @ -1,52 +0,0 @@ | ||||||
| // In production, we register a service worker to serve assets from local cache.
 |  | ||||||
| 
 |  | ||||||
| // This lets the app load faster on subsequent visits in production, and gives
 |  | ||||||
| // it offline capabilities. However, it also means that developers (and users)
 |  | ||||||
| // will only see deployed updates on the "N+1" visit to a page, since previously
 |  | ||||||
| // cached resources are updated in the background.
 |  | ||||||
| 
 |  | ||||||
| // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
 |  | ||||||
| // This link also includes instructions on opting out of this behavior.
 |  | ||||||
| 
 |  | ||||||
| export default function register() { |  | ||||||
|   if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { |  | ||||||
|     window.addEventListener('load', () => { |  | ||||||
|       const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; |  | ||||||
|       navigator.serviceWorker |  | ||||||
|         .register(swUrl) |  | ||||||
|         .then(registration => { |  | ||||||
|           // eslint-disable-next-line no-param-reassign
 |  | ||||||
|           registration.onupdatefound = () => { |  | ||||||
|             const installingWorker = registration.installing; |  | ||||||
|             installingWorker.onstatechange = () => { |  | ||||||
|               if (installingWorker.state === 'installed') { |  | ||||||
|                 if (navigator.serviceWorker.controller) { |  | ||||||
|                   // At this point, the old content will have been purged and
 |  | ||||||
|                   // the fresh content will have been added to the cache.
 |  | ||||||
|                   // It's the perfect time to display a "New content is
 |  | ||||||
|                   // available; please refresh." message in your web app.
 |  | ||||||
|                   console.log('New content is available; please refresh.'); // eslint-disable-line no-console
 |  | ||||||
|                 } else { |  | ||||||
|                   // At this point, everything has been precached.
 |  | ||||||
|                   // It's the perfect time to display a
 |  | ||||||
|                   // "Content is cached for offline use." message.
 |  | ||||||
|                   console.log('Content is cached for offline use.'); // eslint-disable-line no-console
 |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|             }; |  | ||||||
|           }; |  | ||||||
|         }) |  | ||||||
|         .catch(error => { |  | ||||||
|           console.error('Error during service worker registration:', error); |  | ||||||
|         }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function unregister() { |  | ||||||
|   if ('serviceWorker' in navigator) { |  | ||||||
|     navigator.serviceWorker.ready.then(registration => { |  | ||||||
|       registration.unregister(); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  | @ -1,12 +0,0 @@ | ||||||
| import {StrictMode} from "react"; |  | ||||||
| import {createRoot, Root} from "react-dom/client"; |  | ||||||
| import "./styles.css"; |  | ||||||
| import App from "./App.tsx"; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| const root: Root = createRoot(document.getElementById("root")); |  | ||||||
| root.render( |  | ||||||
|     <StrictMode> |  | ||||||
|         <App/> |  | ||||||
|     </StrictMode> |  | ||||||
| ); |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| { |  | ||||||
|   "dependencies": { |  | ||||||
|     "react": "^18.0.0", |  | ||||||
|     "react-dom": "^18.0.0", |  | ||||||
|     "react-scripts": "^5.0.0" |  | ||||||
|   }, |  | ||||||
|   "main": "/index.js", |  | ||||||
|   "devDependencies": {} |  | ||||||
| } |  | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| <!DOCTYPE html> |  | ||||||
| <html lang="en"> |  | ||||||
|   <head> |  | ||||||
|     <meta charset="UTF-8"> |  | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> |  | ||||||
|     <title>A-Maze-R</title> |  | ||||||
|   </head> |  | ||||||
|   <body> |  | ||||||
|     <div id="root"></div> |  | ||||||
|   </body> |  | ||||||
| </html> |  | ||||||
							
								
								
									
										151
									
								
								src/styles.css
									
										
									
									
									
								
							
							
						
						
									
										151
									
								
								src/styles.css
									
										
									
									
									
								
							|  | @ -1,151 +0,0 @@ | ||||||
| * { |  | ||||||
|     box-sizing: border-box; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.maze { |  | ||||||
|     display: table; |  | ||||||
|     border-collapse: collapse; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.row { |  | ||||||
|     display: table-row; |  | ||||||
|     margin: 0; |  | ||||||
|     padding: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.cell { |  | ||||||
|     display: table-cell; |  | ||||||
|     border: 1px solid transparent; |  | ||||||
|     height: 2em; |  | ||||||
|     width: 2em; |  | ||||||
|     padding: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.cell.solution { |  | ||||||
|     background-color: lightgray; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.cell.top { |  | ||||||
|     border-top-color: #000; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.cell.right { |  | ||||||
|     border-right-color: #000; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.cell.bottom { |  | ||||||
|     border-bottom-color: #000; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.cell.left { |  | ||||||
|     border-left-color: #000; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.cell.user { |  | ||||||
|     background-color: hotpink; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.cell.user2 { |  | ||||||
|     border-radius: 0.5em; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.cell.solution.user { |  | ||||||
|     background-color: darkred; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| div.message-banner { |  | ||||||
|     border: 1px solid red; |  | ||||||
|     background-color: #ff000044; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| input:invalid { |  | ||||||
|     border-color: #f00; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| body { |  | ||||||
|     font-family: sans-serif; |  | ||||||
|     margin: 20px; |  | ||||||
|     padding: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| h1 { |  | ||||||
|     margin-top: 0; |  | ||||||
|     font-size: 22px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| h2 { |  | ||||||
|     margin-top: 0; |  | ||||||
|     font-size: 20px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| h3 { |  | ||||||
|     margin-top: 0; |  | ||||||
|     font-size: 18px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| h4 { |  | ||||||
|     margin-top: 0; |  | ||||||
|     font-size: 16px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| h5 { |  | ||||||
|     margin-top: 0; |  | ||||||
|     font-size: 14px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| h6 { |  | ||||||
|     margin-top: 0; |  | ||||||
|     font-size: 12px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| code { |  | ||||||
|     font-size: 1.2em; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| ul { |  | ||||||
|     padding-left: 20px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| * { |  | ||||||
|     box-sizing: border-box; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| body { |  | ||||||
|     font-family: sans-serif; |  | ||||||
|     margin: 20px; |  | ||||||
|     padding: 0; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .square { |  | ||||||
|     background: #fff; |  | ||||||
|     border: 1px solid #999; |  | ||||||
|     float: left; |  | ||||||
|     font-size: 24px; |  | ||||||
|     font-weight: bold; |  | ||||||
|     line-height: 34px; |  | ||||||
|     height: 34px; |  | ||||||
|     margin-right: -1px; |  | ||||||
|     margin-top: -1px; |  | ||||||
|     padding: 0; |  | ||||||
|     text-align: center; |  | ||||||
|     width: 34px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .board-row:after { |  | ||||||
|     clear: both; |  | ||||||
|     content: ''; |  | ||||||
|     display: table; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .status { |  | ||||||
|     margin-bottom: 10px; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .game { |  | ||||||
|     display: flex; |  | ||||||
|     flex-direction: row; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .game-info { |  | ||||||
|     margin-left: 20px; |  | ||||||
| } |  | ||||||
							
								
								
									
										31
									
								
								tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								tsconfig.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | ||||||
|  | { | ||||||
|  |   "compilerOptions": { | ||||||
|  |     "target": "ES2017", | ||||||
|  |     "lib": ["dom", "dom.iterable", "esnext"], | ||||||
|  |     "allowJs": true, | ||||||
|  |     "allowImportingTsExtensions": true, | ||||||
|  |     "skipLibCheck": true, | ||||||
|  |     "strict": true, | ||||||
|  |     "noEmit": true, | ||||||
|  |     "esModuleInterop": true, | ||||||
|  |     "module": "esnext", | ||||||
|  |     "moduleResolution": "bundler", | ||||||
|  |     "resolveJsonModule": true, | ||||||
|  |     "isolatedModules": true, | ||||||
|  |     "jsx": "preserve", | ||||||
|  |     "incremental": true, | ||||||
|  |     "plugins": [ | ||||||
|  |       { | ||||||
|  |         "name": "next" | ||||||
|  |       } | ||||||
|  |     ], | ||||||
|  |     "paths": { | ||||||
|  |       "@/*": ["./src/*"] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "include": [ | ||||||
|  |     "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], | ||||||
|  |   "exclude": [ | ||||||
|  |     "a-maze-r/node_modules" | ||||||
|  |   ] | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue