Add multiplication practice
This commit is contained in:
13
src/app.tsx
13
src/app.tsx
@@ -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>
|
||||||
|
|||||||
@@ -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
16
src/prob.ts
Normal 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
25
src/times.css
Normal 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
99
src/times.tsx
Normal 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>×</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)}>●</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]} × {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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user