update react

This commit is contained in:
Manuel Friedli 2024-12-26 17:38:31 +01:00
parent 804ce323bf
commit 7d4f1151fa
Signed by: manuel
GPG key ID: 41D08ABA75634DA1
40 changed files with 3573 additions and 14078 deletions

View file

@ -1,101 +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/>
<label for="algorithm">Algorithmus:</label>
<select id={"algorithm"}>
<option>wilson</option>
<option>random</option>
</select><br/>
<button type={"submit"}
disabled={state.loading
|| isNaN(width)
|| isNaN(height)
}>{state.loading ? "Loading ..." : "Create Maze!"}
</button>
</form>
);
}

View file

@ -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 <></>;
}

27
src/app/cell.css Normal file
View file

@ -0,0 +1,27 @@
.solution {
background-color: var(--color-maze-cell-solution);
}
.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: hotpink;
}
.solution.user {
background-color: darkred;
}

7
src/app/cell.module.css Normal file
View file

@ -0,0 +1,7 @@
.cell {
display: table-cell;
border: 1px solid transparent;
height: 2em;
width: 2em;
padding: 0;
}

View file

@ -1,6 +1,8 @@
import {MazeCell} from "./model/Maze";
import Coordinates from "./model/Coordinates";
import {MazeCell} from "./model/maze.ts";
import Coordinates from "./model/coordinates.ts";
import {actionClickedCell} from "./state/action.ts";
import styles from "./cell.module.css";
import "./cell.css";
function isMarked(x: number, y: number, marked: Coordinates[]): boolean {
return !!marked.find(e => e.x === x && e.y === y);
@ -8,7 +10,7 @@ function isMarked(x: number, y: number, marked: Coordinates[]): boolean {
export default function Cell({x, y, state, dispatch}) {
const cell: MazeCell = state.maze.grid[y][x];
let classes = "cell r" + y + " c" + x;
let classes = " r" + y + " c" + x;
if (cell.top) classes += " top";
if (cell.right) classes += " right";
if (cell.bottom) classes += " bottom";
@ -17,7 +19,7 @@ export default function Cell({x, y, state, dispatch}) {
const marked = isMarked(x, y, state.userPath);
if (marked) classes += " user";
return (
<div className={classes}
<div className={styles.cell + classes}
onMouseEnter={(e) => {
const leftPressed = e.buttons & 0x1;
if (leftPressed) {

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

216
src/app/globals.css Normal file
View file

@ -0,0 +1,216 @@
: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;
}
@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;
}
}
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);
}
/*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: var(--color-maze-border);*/
/*}*/
/*div.cell.right {*/
/* border-right-color: var(--color-maze-border);*/
/*}*/
/*div.cell.bottom {*/
/* border-bottom-color: var(--color-maze-border);*/
/*}*/
/*div.cell.left {*/
/* border-left-color: var(--color-maze-border);*/
/*}*/
/*div.cell.user {*/
/* background-color: hotpink;*/
/*}*/
/*div.cell.user2 {*/
/* border-radius: 0.5em;*/
/*}*/
/*div.cell.solution.user {*/
/* background-color: darkred;*/
/*}*/
input:invalid {
border-color: #f00;
}
body {
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;
}
.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;
}
/*:root {*/
/* --background: #ffffff;*/
/* --foreground: #171717;*/
/*}*/
/*@media (prefers-color-scheme: dark) {*/
/* :root {*/
/* --background: #0a0a0a;*/
/* --foreground: #ededed;*/
/* }*/
/*}*/
/*html,*/
/*body {*/
/* max-width: 100vw;*/
/* overflow-x: hidden;*/
/*}*/
/*body {*/
/* color: var(--foreground);*/
/* background: var(--background);*/
/* font-family: Arial, Helvetica, sans-serif;*/
/* -webkit-font-smoothing: antialiased;*/
/* -moz-osx-font-smoothing: grayscale;*/
/*}*/
/** {*/
/* box-sizing: border-box;*/
/* padding: 0;*/
/* margin: 0;*/
/*}*/
/*a {*/
/* color: inherit;*/
/* text-decoration: none;*/
/*}*/
/*@media (prefers-color-scheme: dark) {*/
/* html {*/
/* color-scheme: dark;*/
/* }*/
/*}*/

21
src/app/input-form.css Normal file
View 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);
}

View 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;
}

110
src/app/input-form.tsx Normal file
View file

@ -0,0 +1,110 @@
import {useState} from 'react';
import ValidatingInputNumberField from "./validating-input-number-field.tsx";
import {actionLoadedMaze, actionLoadingFailed, actionStartedLoading} from "./state/action.ts";
import styles from "./input-form.module.css";
import "./input-form.css";
export default function InputForm({state, dispatch}) {
const [width, setWidth] = useState(10);
const [height, setHeight] = useState(10);
const [id, setId] = useState(null as number);
const [algorithm, setAlgorithm] = useState('wilson');
const handleSubmit = (e) => {
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 = 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}>
<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
View 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
View 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;
}

View file

@ -1,5 +1,5 @@
import Cell from "./Cell.tsx";
import Cell from "./cell.tsx";
import styles from "./maze.module.css";
export default function Maze({state, dispatch}) {
if (!state.maze) {
@ -12,10 +12,10 @@ export default function Maze({state, dispatch}) {
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>);
maze.push(<div key={`r${y}`} className={styles.row}>{row}</div>);
}
return (
<div className={"maze"}>
<div className={styles.maze}>
{maze}
</div>
);

View file

@ -0,0 +1,10 @@
.banner {
height: 2em;
}
.empty {
}
.message {
border: 1px solid red;
background-color: #ff000044;
}

View file

@ -0,0 +1,18 @@
import {actionClosedMessageBanner} from "./state/action.ts";
import styles from "./message-banner.module.css";
export default function MessageBanner({state, dispatch}) {
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>);
}

View file

@ -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[][]
}
@ -15,4 +16,4 @@ export interface MazeCell {
bottom: boolean,
left: boolean,
solution: boolean
}
}

179
src/app/page.module.css Normal file
View 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();*/
/* }*/
/*}*/

View file

@ -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>
);
}

View file

@ -1,4 +1,4 @@
import Maze from "../model/Maze";
import Maze from "../model/maze.ts";
export interface Action {
type: string,
@ -57,4 +57,4 @@ export function actionClickedCell(x: number, y: number): Action {
x,
y
}
}
}

View file

@ -10,7 +10,7 @@ import {
ID_ACTION_TOGGLED_SHOW_SOLUTION
} from "./action.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 {

View file

@ -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,
@ -15,4 +15,4 @@ export const INITIAL_STATE: State = {
maze: null,
showSolution: false,
userPath: []
};
};

View file

@ -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)) {
let 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 {

View file

@ -35,7 +35,7 @@ export default function ValidatingInputNumberField({
max={constraints.max || null}
disabled={disabled}
/>
{!!error && <span>{error}</span>}
<span>{error}</span>
</>
);
}

View file

@ -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();
});
}
}

View file

@ -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>
);

View file

@ -1,9 +0,0 @@
{
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-scripts": "^5.0.0"
},
"main": "/index.js",
"devDependencies": {}
}

View file

@ -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>

View file

@ -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;
}