JSFiddle - React, Tailwind, and code Playground

by Josh Pullen

CSS

body {
  width: 100vw;
  height: 100vh;
  margin: 0;
  
  display: flex;
  
  font-family: sans-serif;
}

.grid {
  display: grid;
  width: 100vmin;
  height: 100vmin;
  grid-template-rows: repeat(10, 1fr);
  grid-template-columns: repeat(10, 1fr);
}

.grid > button {
  border: none;
}

.homeInfo {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  
  font-size: 24px;
  text-align: center;
}
.winText {
  color: rgb(6, 138, 72);
  margin-top: 16px;
  font-weight: bold;
}
.reset {
  position: absolute;
  bottom: 8px;
}

.number {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  
  font-size: 96px;
  text-align: center;
}

.inputs {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
.inputRow {
  display: flex;
  align-items: center;
  font-size: 32px;
  padding: 8px;
  margin: 2px 8px;
  border-radius: 4px;
}
.inputRow > input {
  width: 80px;
  font-size: 24px;
  border-radius: 4px;
  padding: 8px;
  border: none;
  background: rgba(0, 0, 0, 0.1);
  text-align: center;
  color: inherit;
}
.inputRow > span {
  margin: 0 12px;
}
.inputRow--yes {
  background: rgb(6, 138, 72);
  color: #fff;
}
.inputRow--duplicate {
  background: #f4ff91;
}

/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Firefox */
input[type=number] {
  -moz-appearance: textfield;
}

JavaScript

import { html, render, useState } from "https://unpkg.com/htm/preact/standalone.module.js";

let defaultInputState = [];
defaultInputState[0] = [];
for (let i = 2; i <= 100; i++) {
	const pairCount = factorPairs(i).length;
  const inputRows = new Array(pairCount).fill().map(() => ["", ""]);
  defaultInputState[i - 1] = inputRows;
}

function factorPairs(n) {
	let result = [];
	for (let i = 1; i <= Math.sqrt(n); i++) {
  	if (n % i === 0) {
    	result.push([i, n / i]);
    }
  }
  return result;
}

function validatePairs(n, pairs) {
  let result = [];
  let usedDivisors = [];
	for (let i = 0; i < pairs.length; i++) {
  	const pair = pairs[i];
    
    let valid = (pair[0] * pair[1] === n) ? "yes" : "no";
    if (isNaN(pair[0])) valid = "no";
    if (isNaN(pair[1])) valid = "no";
    if (pair[0] % 1 !== 0) valid = "no";
    if (pair[1] % 1 !== 0) valid = "no";
    if (valid === "yes") {
    	if (usedDivisors.includes(pair[0])) valid = "duplicate";
    	if (usedDivisors.includes(pair[1])) valid = "duplicate";
    }
  	result.push(valid);
    
    if (valid === "yes") {
    	usedDivisors.push(pair[0]);
      usedDivisors.push(pair[1]);
    }
  }
  
  return result;
}

function App() {
	const [screen, setScreen] = useState({ name: "home" });
  const [inputs, setInputs] = useState(defaultInputState);
  
  const setInput = (num, pairIndex, inputIndex, value) => {
  	setInputs([
    	...inputs.slice(0, num - 1),
      [
      	...inputs[num - 1].slice(0, pairIndex),
        [
        	...inputs[num - 1][pairIndex].slice(0, inputIndex),
          value,
          ...inputs[num - 1][pairIndex].slice(inputIndex + 1)
        ],
        ...inputs[num - 1].slice(pairIndex + 1)
      ],
      ...inputs.slice(num)
    ]);
  }
  
  const validationResults = inputs.map((pairs, i) => (
  	validatePairs(i + 1, pairs.map(pair => pair.map(n => n === "" ? NaN : Number(n))))
  ));
  
  const overallProgress = validationResults.flat().reduce((a, b) => a + (b === "yes"), 0);
	const...