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
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.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",
|
||||
"description": "The A-Maze-Ing Generator!",
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject"
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"next": "15.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@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 {
|
||||
id: string,
|
||||
|
@ -6,6 +6,7 @@ export default interface Maze {
|
|||
height: number,
|
||||
start: Coordinates,
|
||||
end: Coordinates,
|
||||
algorithm: string,
|
||||
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';
|
||||
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";
|
||||
'use client'
|
||||
|
||||
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 hasValidMaze = !!state.maze;
|
||||
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}
|
||||
dispatch={dispatch}/>
|
||||
<InputForm state={state}
|
||||
dispatch={dispatch}/>
|
||||
{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"}
|
||||
onChange={(e) => {
|
||||
dispatch(actionToggledShowSolution(e.target.checked));
|
||||
|
@ -27,6 +33,6 @@ export default function App() {
|
|||
dispatch={dispatch}/>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import Maze from "../model/Maze";
|
||||
import Maze from "../model/maze.ts";
|
||||
|
||||
export interface Action {
|
||||
type: string,
|
||||
|
||||
[key: string]: any
|
||||
[key: string]: boolean | number | string | object | null | undefined;
|
||||
}
|
||||
|
||||
export const ID_ACTION_STARTED_LOADING = 'started_loading';
|
|
@ -9,8 +9,9 @@ import {
|
|||
ID_ACTION_STARTED_LOADING,
|
||||
ID_ACTION_TOGGLED_SHOW_SOLUTION
|
||||
} 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) {
|
||||
case ID_ACTION_STARTED_LOADING: {
|
||||
return {
|
||||
|
@ -24,7 +25,7 @@ export default function reduce(state: State, action: Action) {
|
|||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
maze: action.maze,
|
||||
maze: action.maze as Maze,
|
||||
userPath: []
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +39,7 @@ export default function reduce(state: State, action: Action) {
|
|||
case ID_ACTION_TOGGLED_SHOW_SOLUTION: {
|
||||
return {
|
||||
...state,
|
||||
showSolution: action.value
|
||||
showSolution: action.value as boolean
|
||||
}
|
||||
}
|
||||
case ID_ACTION_CLOSED_MESSAGE_BANNER: {
|
||||
|
@ -49,7 +50,7 @@ export default function reduce(state: State, action: Action) {
|
|||
}
|
||||
case ID_ACTION_CLICKED_CELL: {
|
||||
// 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: {
|
||||
throw new Error(`Unknown action: ${action.type}`);
|
|
@ -1,5 +1,5 @@
|
|||
import Coordinates from "../model/Coordinates";
|
||||
import Maze from "../model/Maze";
|
||||
import Coordinates from "../model/coordinates.ts";
|
||||
import Maze from "../model/maze.ts";
|
||||
|
||||
export interface State {
|
||||
errorMessage: string | null,
|
|
@ -1,15 +1,16 @@
|
|||
import {State} from "./state";
|
||||
import Coordinates from "../model/Coordinates";
|
||||
import {MazeCell} from "../model/Maze";
|
||||
import Coordinates from "../model/coordinates.ts";
|
||||
import {MazeCell} from "../model/maze.ts";
|
||||
|
||||
export default function handleUserClicked(state: State, x: number, y: number): 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)
|
||||
// and that's not blocked by a wall. Now let's see.
|
||||
if (-1 === state.userPath.findIndex(step => step.x === x && step.y === y)) {
|
||||
// The clicked cell is not yet part of the userpath --> add it.
|
||||
// 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 {
|
||||
...state,
|
||||
userPath: [...state.userPath, {x, y}],
|
||||
|
@ -17,7 +18,7 @@ export default function handleUserClicked(state: State, x: number, y: number): S
|
|||
};
|
||||
} else {
|
||||
// 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) {
|
||||
// Yes, it's the last cell of the userpath --> remove it.
|
||||
return {
|
||||
|
@ -36,7 +37,7 @@ function isClickAllowed(x: number, y: number, state: State): boolean {
|
|||
const lastCoordsFromUserPath = getLastCoordsFromUserPath(state);
|
||||
if (!lastCoordsFromUserPath) {
|
||||
// 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) {
|
||||
// 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 {
|
||||
return state.maze.grid[coords.y][coords.x];
|
||||
return state.maze!.grid[coords.y][coords.x];
|
||||
}
|
||||
|
||||
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…
Reference in a new issue