Add multiplication practice

This commit is contained in:
2024-08-19 20:51:15 -04:00
parent 40c3c29019
commit 609fc12418
5 changed files with 178 additions and 3 deletions

View File

@@ -1,16 +1,25 @@
import { useState } from 'preact/hooks'
import { GCF } from './gcf' import { GCF } from './gcf'
import { LCM } from './lcm' import { LCM } from './lcm'
import { TimesPractice } from './times'
import Navbar from './navbar' import Navbar from './navbar'
import { BrowserRouter, Navigate, Routes, Route } from 'react-router-dom' import { BrowserRouter, Navigate, Routes, Route } from 'react-router-dom'
import './app.css' import './app.css'
export function App() { export function App() {
const [ maxNum, setMaxNum ] = useState(200)
function onMaxNumSet(newMax : number) {
setMaxNum(newMax)
}
return <> return <>
<BrowserRouter> <BrowserRouter>
<Navbar /> <Navbar maxNum={maxNum} onMaxNumChanged={onMaxNumSet} />
<Routes> <Routes>
<Route path="/" element={<Navigate to="/gcf" />} /> <Route path="/" element={<Navigate to="/gcf" />} />
<Route path="/gcf" element={<GCF />} /> <Route path="/gcf" element={<GCF maxNum={maxNum} />} />
<Route path="/times" element={<TimesPractice maxNum={12} />} />
<Route path="/lcm" element={<LCM />} /> <Route path="/lcm" element={<LCM />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>

View File

@@ -1,13 +1,29 @@
import { useState } from 'preact/hooks' import { useState } from 'preact/hooks'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
function Navbar() { export type NavbarSettings = {
maxNum : number
onMaxNumChanged : (newMaxNum : number) => void
}
function Navbar({maxNum, onMaxNumChanged} : NavbarSettings) {
const [ show, setShow ] = useState(false); const [ show, setShow ] = useState(false);
const [ displayedMax, setDisplayedMax ] = useState<number>(maxNum)
const handleNavClick = () : void => { const handleNavClick = () : void => {
setShow(false) setShow(false)
} }
const handleSliderInput = (e : Event) : void => {
const newMaxNum = Number((e.target as HTMLInputElement).value)
onMaxNumChanged(newMaxNum)
}
const onSliderChange = (e : Event) : void => {
const newMaxNum = Number((e.target as HTMLInputElement).value)
setDisplayedMax(newMaxNum)
}
return ( return (
<nav class="navbar navbar-expand-sm bg-body-tertiary"> <nav class="navbar navbar-expand-sm bg-body-tertiary">
<div class="container-fluid"> <div class="container-fluid">
@@ -20,10 +36,20 @@ function Navbar() {
<li class="nav-item"> <li class="nav-item">
<NavLink to="/gcf" title="Greatest Common Factor" onClick={handleNavClick} className={({ isActive, isPending }) => "nav-link " + (isPending ? "" : isActive ? "active" : "")}>GCF</NavLink> <NavLink to="/gcf" title="Greatest Common Factor" onClick={handleNavClick} className={({ isActive, isPending }) => "nav-link " + (isPending ? "" : isActive ? "active" : "")}>GCF</NavLink>
</li> </li>
<li class="nav-item">
<NavLink to="/times" title="Times" onClick={handleNavClick} className={({ isActive, isPending }) => "nav-link " + (isPending ? "" : isActive ? "active" : "")}>Times</NavLink>
</li>
<li class="nav-item"> <li class="nav-item">
<NavLink to="/lcm" title="Least Common Multiple" onClick={handleNavClick} className={({ isActive, isPending }) => "nav-link " + (isPending ? "" : isActive ? "active" : "")}>LCM</NavLink> <NavLink to="/lcm" title="Least Common Multiple" onClick={handleNavClick} className={({ isActive, isPending }) => "nav-link " + (isPending ? "" : isActive ? "active" : "")}>LCM</NavLink>
</li> </li>
</ul> </ul>
<div className="nav-item ms-auto me-6 g-3 row align-items-center">
<label className="col-form-label col-sm-4 col-form-label-sm ge-2 flex-shrink-0" for="maxNum">Max:</label>
<div className="col-sm-4 flex-shrink-0">
<input className="form-range" type="range" id="maxNum" step={10} value={maxNum} max={200} min={50} onInput={handleSliderInput} onChange={onSliderChange} />
</div>
<label className="col-form-label col-sm-2 ms-auto">{displayedMax}</label>
</div>
</div> </div>
</div> </div>
</nav> </nav>

16
src/prob.ts Normal file
View File

@@ -0,0 +1,16 @@
namespace Prob {
export type Probability<T> = [ number, T ]
type NonEmptyArray<T> = [ T, ...T[] ]
export type ProbabilitySet<T> = NonEmptyArray<Probability<T>>
export const chooseRandom = <T>(probabilities : ProbabilitySet<T>) : T => {
let r = Math.random(), accum = 0
for (let i = 0; i < probabilities.length; i++) {
if ((r >= accum) && (r < accum + probabilities[i][0]))
return probabilities[i][1]
accum += probabilities[i][0]
}
return probabilities[probabilities.length - 1][1]
}
}
export default Prob

25
src/times.css Normal file
View File

@@ -0,0 +1,25 @@
.solution {
padding: 16px;
border-radius: 12px;
background: white;
color: black;
clear: both;
display: flex;
justify-content: center;
}
.timesCircle {
cursor: pointer;
min-width: 2.0em;
min-height: 1.5em;
}
.timesCircleLit {
color: lightblue;
}
.timesSolution,
.timesRows {
width: 40px;;
}

99
src/times.tsx Normal file
View File

@@ -0,0 +1,99 @@
import { useEffect, useState } from 'preact/hooks'
import './times.css'
interface TimesPracticeParams {
maxNum : number
}
interface SolutionParams {
rows : number
cols : number
}
function Solution({rows,cols}:SolutionParams) {
const [coords, setCoords] = useState<[number,number]|null>(null)
return (
<div className="solution text-center">
<table>
<tr><td>&times;</td><td colspan={cols}>{coords !== null ? coords[1] + 1 : cols}</td></tr>
{Array(rows).fill(0).map((_, i)=>(<tr>{i === 0 ? <td className="timesRows" rowspan={rows}>{coords !== null ? coords[0] + 1 : rows}</td> : <></>}{Array(cols).fill(0).map((_, j)=><td className={"timesCircle" + (coords !== null && (i <= coords[0]) && (j <= coords[1]) ? " timesCircleLit" : "")} title={String(cols*i+j)} onMouseOver={()=>setCoords([i,j])} onMouseOut={()=>setCoords(null)}>&#x25cf;</td>)}{i === 0 ? <td className="timesSolution" rowspan={rows+1}>={coords !== null ? String((coords[0] + 1) * (coords[1] + 1)) : rows * cols}</td> : <></>}</tr>))}
</table>
</div>
)
}
export function TimesPractice({maxNum} : TimesPracticeParams) {
const getRandomInt = (max : number) : number => Math.floor(Math.random() * max + 1)
const chooseFactors = () : [number, number] => {
return [ getRandomInt(12), getRandomInt(12) ]
}
const doCheck = (e : Event) : void => {
e.preventDefault()
if (factors === null) return
const rightAnswer = factors[0] * factors[1]
const yourAnswer = (document.getElementById("inlineFormInputResponse") as HTMLInputElement).value
if (rightAnswer == Number(yourAnswer)) {
setCorrect(true)
setFeedback(true)
} else {
setFeedback(true)
}
}
const doNext = (e : Event) : void => {
e.preventDefault()
setFeedback(false)
setCorrect(false)
setFactors(chooseFactors())
setSolution(null)
document.forms[0].reset()
}
const doSolution = (e : Event) : void => {
e.preventDefault()
if (factors === null) return
if (solution === null) {
var answerBox = document.getElementById("inlineFormInputResponse") as HTMLInputElement
answerBox.value = String(factors[0] * factors[1])
setSolution(<Solution rows={factors[0]} cols={factors[1]} />)
setCorrect(true)
} else {
setSolution(null)
}
}
useEffect(()=>{
setFactors(chooseFactors())
}, [ maxNum ])
const [factors, setFactors] = useState<[number,number]|null>(null)
const [correct, setCorrect] = useState(false)
const [feedback, setFeedback] = useState(false)
const [solution, setSolution] = useState<JSX.Element|null>(null)
return (
<div className="px-4 py-5 my-5 text-center">
<h1 className="my-5 d-none d-sm-block">Multiplication Practice</h1>
{factors === null ? <p>Loading...</p> : <p className="fs-3">What is {factors[0]} &times; {factors[1]}?</p>}
<form className="row row-cols-lg-auto g-3 align-items-center justify-content-center">
<div className="col-12">
<label className="visually-hidden" for="inlineFormInputResponse">Response</label>
<div className="input-group">
<input type="number" size={3} autocomplete="off" className="form-control text-center no-arrows" id="inlineFormInputResponse" />
</div>
</div>
<div className="col-12">
<button type="submit" onClick={correct ? doNext : doCheck} className="btn btn-primary">{correct ? "Next" : "Check"}</button>
<button type="submit" onClick={doSolution} className="ms-2 btn btn-secondary">{solution !== null ? "Hide" : "Solve"}</button>
</div>
</form>
<div className={feedback ? "visible" : "invisible"}>
<div className={correct ? "alert alert-success m-4" : "alert alert-danger m-4"}>{correct ? "Correct" : "Try again!"}</div>
</div>
{solution !== null ? solution : <></>}
</div>
)
}