website

source code for the ttmaths.uk website
Log | Files | Refs | README

commit ac571718ef71218b477cc68775093dcb8793f16e
parent 540ff42219dfb6a76d6cd86bd99a458fc54f1b23
Author: hrryt <harry@mayesfield.uk>
Date:   Wed, 29 Jul 2026 22:28:15 +0100

Refactor to allow random question list specification in data/

Diffstat:
Dsrc/components/QItem.jsx | 21---------------------
Dsrc/components/QList.jsx | 41-----------------------------------------
Dsrc/components/QuadraticEquationQItem.jsx | 41-----------------------------------------
Dsrc/components/QuadraticEquationQList.jsx | 43-------------------------------------------
Dsrc/components/QuadraticEquationQuestionSet.jsx | 14--------------
Asrc/components/Question.jsx | 10++++++++++
Asrc/components/QuestionList.jsx | 5+++++
Asrc/components/QuestionPanel.jsx | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/TestQuestionPanel.jsx | 7+++++++
Asrc/components/questions/AddingFractionsQuestion.jsx | 42++++++++++++++++++++++++++++++++++++++++++
Asrc/components/questions/QuadraticEquationQuestion.jsx | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/data/test-question-panel.js | 17+++++++++++++++++
Dsrc/scripts/contexts.js | 5-----
Msrc/scripts/react-roots.js | 4++--
Asrc/scripts/utils.js | 25+++++++++++++++++++++++++
Msrc/styles/index.css | 32+++++---------------------------
Mwww/index.html | 2+-
17 files changed, 246 insertions(+), 195 deletions(-)

diff --git a/src/components/QItem.jsx b/src/components/QItem.jsx @@ -1,21 +0,0 @@ -import * as React from 'react'; -import Equation from './Equation.jsx'; -import { ShowAnswerContext, QListContext } from '../scripts/contexts.js'; - -export default function QItem({ question, answer }) { - const showAnswer = React.useContext(ShowAnswerContext); - const inQList = React.useContext(QListContext); - - const item = ( - <div className="QItem"> - <div className="Question"> - <Equation equation={question} /> - </div> - <div className={"Answer" + (showAnswer ? "" : " AnswerInvisible")}> - <Equation equation={answer} /> - </div> - </div> - ); - - return inQList ? <li>{item}</li> : item; -} diff --git a/src/components/QList.jsx b/src/components/QList.jsx @@ -1,41 +0,0 @@ -import * as React from 'react'; -import { QListContext, ShowAnswerContext, RefreshContext } from '../scripts/contexts.js'; - -function QuestionSet({ children }) { - const [showAnswers, setShowAnswers] = React.useState(false); - const [refresh, setRefresh] = React.useState(false); - function revealAnswers() { setShowAnswers(!showAnswers) }; - function triggerRefresh() { setRefresh(!refresh) }; - - return ( - <div className="QuestionSet"> - <div className="ButtonPanel"> - <button className="reveal-button" onClick={revealAnswers}> - {showAnswers ? "Hide" : "Show"} Answers - </button> - <button className="refresh-button" onClick={triggerRefresh}> - Refresh Questions - </button> - </div> - <ShowAnswerContext value={showAnswers}> - <RefreshContext value={refresh}> - {children} - </RefreshContext> - </ShowAnswerContext> - </div> - ); -} - -export default function QList({ children }) { - const inQList = React.useContext(QListContext); - - const list = ( - <ol className={"QList" + (inQList ? " QListInner" : " QListOuter")}> - <QListContext value="true"> - {children} - </QListContext> - </ol> - ); - - return inQList ? <li>{list}</li> : <QuestionSet>{list}</QuestionSet>; -} diff --git a/src/components/QuadraticEquationQItem.jsx b/src/components/QuadraticEquationQItem.jsx @@ -1,41 +0,0 @@ -import * as React from 'react'; -import QItem from './QItem.jsx'; - -function formatNumber(x, showPlus, showOne, showZero, suffix='') { - if (!showZero && x == 0) { return ''; } - plus = showPlus ? '+' : ''; - sign = x < 0 ? '-' : plus; - x = Math.abs(x); - showOne || x == 1 && (x = ''); - return sign + x + suffix; -} - -function formatQuadraticEquation(a, b, c) { - const fa = formatNumber(a, showPlus=false, showOne=false, showZero=false, suffix='x^2'); - const fb = formatNumber(b, showPlus=true, showOne=false, showZero=false, suffix='x'); - const fc = formatNumber(c, showPlus=true, showOne=true, showZero=false); - return `${fa} ${fb} ${fc} = 0`; -} - -function formatRoots(root1, root2) { - const froot2 = formatNumber(root2, showPlus=false, showOne=true, showZero=true); - if (root1 == root2) { return `x = ${froot2}`; } - if (Math.abs(root1) == root2) { return `x = plus.minus ${root2}`; } - const froot1 = formatNumber(root1, showPlus=false, showOne=true, showZero=true); - return `x = ${froot1} thick "or" thick x = ${froot2}`; -} - -function getQuadraticEquation(root1, root2, a) { - const b = a * -(root1 + root2); - const c = a * root1 * root2; - - const equation = formatQuadraticEquation(a, b, c); - const roots = formatRoots(root1, root2); - - return [equation, roots]; -} - -export default function QuadraticEquationQItem({ roots, a = 1 }) { - const [question, answer] = getQuadraticEquation(roots[0], roots[1], a); - return <QItem question={question} answer={answer} />; -} diff --git a/src/components/QuadraticEquationQList.jsx b/src/components/QuadraticEquationQList.jsx @@ -1,43 +0,0 @@ -import * as React from 'react'; -import QList from './QList.jsx'; -import QuadraticEquationQItem from './QuadraticEquationQItem.jsx'; -import { RefreshContext } from '../scripts/contexts.js'; - -function getRandomInt(min, range) { - return min + Math.floor(range * Math.random()); -} - -function getRandomRoots() { - return [getRandomInt(-9, 19), getRandomInt(-9, 19)].sort((a, b) => a - b); -} - -function getUniqueRoots(n) { - const listRoots = Array(); - - for (let i = 0; i < n; i++) { - roots = getRandomRoots(); - while (listRoots.some((v, i) => v[0] == roots[0] && v[1] == roots[1])) { - roots = getRandomRoots(); - } - listRoots.push(roots); - } - - return listRoots; -} - -export default function QuadraticEquationQList({ n, a = 1 }) { - const [listRoots, setListRoots] = React.useState(getUniqueRoots(10)); - const refresh = React.useContext(RefreshContext); - - React.useEffect(() => { - setListRoots(getUniqueRoots(10)); - }, [refresh]); - - return ( - <QList> - {Array.from({ length: n }).map((v, i) => { - return <QuadraticEquationQItem roots={listRoots[i]} a={a} />; - })} - </QList> - ); -} diff --git a/src/components/QuadraticEquationQuestionSet.jsx b/src/components/QuadraticEquationQuestionSet.jsx @@ -1,14 +0,0 @@ -import * as React from 'react'; -import QList from './QList.jsx'; -import QuadraticEquationQList from './QuadraticEquationQList.jsx'; - -export default function QuadraticEquationQuestionSet() { - return ( - <QList> - <QuadraticEquationQList n="5" /> - <QuadraticEquationQList n="6" a="2" /> - <QuadraticEquationQList n="7" a="3" /> - <QuadraticEquationQList n="5" a="4" /> - </QList> - ); -} diff --git a/src/components/Question.jsx b/src/components/Question.jsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import Equation from './Equation.jsx'; + +export default function Question({ question, answer, showAnswer }) { + return ( + <li className={"Question" + (showAnswer ? " Answer" : "")}> + <Equation equation={showAnswer ? answer : question} /> + </li> + ); +} diff --git a/src/components/QuestionList.jsx b/src/components/QuestionList.jsx @@ -0,0 +1,5 @@ +import * as React from 'react'; + +export default function QuestionList({ children }) { + return <ol className="QuestionList">{children}</ol>; +} diff --git a/src/components/QuestionPanel.jsx b/src/components/QuestionPanel.jsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import QuestionList from './QuestionList.jsx'; +import { getRandomInt, getRandomNumber } from '../scripts/utils.js'; + +import AddingFractionsQuestion from './questions/AddingFractionsQuestion.jsx'; +import QuadraticEquationQuestion from './questions/QuadraticEquationQuestion.jsx'; + +function chooseQuestionComponent(type) { + switch (type) { + case 'quadratic-equation': + return QuadraticEquationQuestion; + case 'adding-fractions': + return AddingFractionsQuestion; + } +} + +function renderQuestion(seed, showAnswer) { + return (question, i) => { + const QuestionComponent = chooseQuestionComponent(question.type); + return <QuestionComponent key={i} seed={seed+i} showAnswer={showAnswer} parameters={question.parameters || {}} />; + }; +} + +function getQuestionList(id, questions, showAnswers, seed) { + return ( + <QuestionList key={id}> + {questions.map(renderQuestion(seed, showAnswers))} + </QuestionList> + ); +} + +function shuffleArray(arr, n, seed) { + const max = arr.length - 1; + return Array.from({ length: n }).map(() => arr[getRandomInt(0, max, ++seed)]); +} + +export default function QuestionPanel({ questionLists }) { + const [showAnswers, setShowAnswers] = React.useState(false); + const [seed , setSeed ] = React.useState(Math.random()); + + function revealAnswers() { setShowAnswers(!showAnswers); } + function triggerRefresh() { setSeed(getRandomNumber(seed)); } + + let current_seed = seed; + const questions = questionLists.map(questionList => { + const { id: id, n: n, questions: unique_questions } = questionList; + const questions = shuffleArray(unique_questions, n, current_seed); + const randomQuestionList = getQuestionList(id, questions, showAnswers, current_seed); + current_seed += n; + return randomQuestionList; + }); + + return ( + <div className="QuestionPanel"> + <div className="ButtonPanel"> + <button className="RevealButton" onClick={revealAnswers}> + {showAnswers ? "Hide" : "Show"} Answers + </button> + <button className="RefreshButton" onClick={triggerRefresh}> + Refresh Questions + </button> + </div> + <div className="QuestionSet"> + {questions} + </div> + </div> + ); +} diff --git a/src/components/TestQuestionPanel.jsx b/src/components/TestQuestionPanel.jsx @@ -0,0 +1,7 @@ +import * as React from 'react'; +import QuestionPanel from './QuestionPanel.jsx'; +import { questionLists } from '../data/test-question-panel.js'; + +export default function TestQuestionPanel() { + return <QuestionPanel questionLists={questionLists} />; +} diff --git a/src/components/questions/AddingFractionsQuestion.jsx b/src/components/questions/AddingFractionsQuestion.jsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import Question from '../Question.jsx'; +import { getRandomInt, getHCF } from '../../scripts/utils.js'; + +function formatFraction(num, denom) { + if (denom == 1) { return `${num}`; } + return num + '/' + denom; +} + +function simplifyFraction(num, denom) { + const hcf = getHCF(num, denom); + return [num / hcf, denom / hcf]; +} + +function getRandomFraction(seed) { + const num0 = getRandomInt(1, 5, seed); + const denom0 = getRandomInt(2, 10, seed + .05); + const [num, denom] = simplifyFraction(num0, denom0); + if (denom == 1) { return getRandomFraction(seed + .1); } + return [num, denom]; +} + +function getRandomFractions(seed) { + const [num1, denom1] = getRandomFraction(seed); + const [num2, denom2] = getRandomFraction(seed + .5); + return [num1, denom1, num2, denom2]; +} + +export default function AddingFractionsQuestion({ seed, showAnswer, parameters }) { + const [num1, denom1, num2, denom2] = getRandomFractions(seed); + + const question = formatFraction(num1, denom1) + ' + ' + formatFraction(num2, denom2); + + const hcf = getHCF(denom1, denom2); + const sum = (num1 * denom2 + num2 * denom1) / hcf; + const lcm = denom1 * denom2 / hcf; + const [num, denom] = simplifyFraction(sum, lcm); + + const answer = formatFraction(num, denom); + + return <Question question={question} answer={answer} showAnswer={showAnswer} />; +} diff --git a/src/components/questions/QuadraticEquationQuestion.jsx b/src/components/questions/QuadraticEquationQuestion.jsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import Question from '../Question.jsx'; +import { getRandomInt, getRandomBoolean, formatNumber } from '../../scripts/utils.js'; + +function formatQuadraticExpression(a, b, c) { + const fa = formatNumber(a, showPlus=false, showOne=false, showZero=false, suffix='x^2'); + const fb = formatNumber(b, showPlus=true, showOne=false, showZero=false, suffix='x'); + const fc = formatNumber(c, showPlus=true, showOne=true, showZero=false); + return `${fa} ${fb} ${fc}`; +} + +function formatRoots(root1, root2) { + const froot2 = formatNumber(root2, showPlus=false, showOne=true, showZero=true); + if (root1 == root2) { return `x = ${froot2}`; } + if (Math.abs(root1) == root2) { return `x = plus.minus ${root2}`; } + const froot1 = formatNumber(root1, showPlus=false, showOne=true, showZero=true); + return `x = ${froot1} thick "or" thick x = ${froot2}`; +} + +function formatFactor(root) { + if (root == 0) { return "x"; } + const froot = formatNumber(root, showPlus=true, showOne=true, showZero=false); + return `(x${froot})`; +} + +function sortFactors(root1, root2) { + if (root1 > 0 && root2 < 0) { return -1; } + if (root1 < 0 && root2 > 0) { return +1; } + return Math.abs(root1) - Math.abs(root2); +} + +function formatFactors(root1, root2, a) { + const fa = formatNumber(a, showPlus=false, showOne=false, showZero=true); + [root1, root2] = [root1, root2].sort(sortFactors); + const froot2 = formatFactor(root2); + if (root1 == root2) { return `${fa}${froot2}^2`; } + const froot1 = formatFactor(root1); + return `${fa}${froot1}${froot2}`; +} + +function getRandomRoots(seed) { + return [ + getRandomInt(-9, 9, seed), + getRandomInt(-9, 9, seed + .1), + ].sort((a, b) => a - b); +} + +export default function QuadraticEquationQuestion({ seed, showAnswer, parameters }) { + const { + a: a = getRandomInt(1, 3, seed), + factorise: factorise = getRandomBoolean(seed + .1), + } = parameters; + + const [root1, root2] = getRandomRoots(seed + .2); + + const b = a * -(root1 + root2); + const c = a * root1 * root2; + + const quadratic = formatQuadraticExpression(a, b, c); + question = factorise ? `"Factorise" thick ${quadratic}` : `${quadratic} = 0`; + const answer = factorise ? formatFactors(root1, root2, a) : formatRoots(root1, root2); + + return <Question question={question} answer={answer} showAnswer={showAnswer} />; +} diff --git a/src/data/test-question-panel.js b/src/data/test-question-panel.js @@ -0,0 +1,17 @@ +export const questionLists = [{ + id: 0, + n: 10, + questions: [{ + type: 'quadratic-equation', + parameters: { factorise: false } + }, { + type: 'quadratic-equation', + parameters: { a: 1, factorise: true } + }] +}, { + id: 1, + n: 5, + questions: [{ + type: 'adding-fractions', + }] +}] diff --git a/src/scripts/contexts.js b/src/scripts/contexts.js @@ -1,5 +0,0 @@ -import { createContext } from 'react'; - -export const QListContext = createContext(false); -export const RefreshContext = createContext(false); -export const ShowAnswerContext = createContext(false); diff --git a/src/scripts/react-roots.js b/src/scripts/react-roots.js @@ -1,7 +1,7 @@ import { createElement } from 'react'; import { createRoot } from 'react-dom/client'; -import QuadraticEquationQuestionSet from '../components/QuadraticEquationQuestionSet.jsx'; +import TestQuestionPanel from '../components/TestQuestionPanel.jsx'; function renderComponent(id, component) { node = document.getElementById(id); @@ -9,4 +9,4 @@ function renderComponent(id, component) { root.render(createElement(component)); } -renderComponent('quadratic-equation-question-set', QuadraticEquationQuestionSet); +renderComponent('test-question-panel', TestQuestionPanel); diff --git a/src/scripts/utils.js b/src/scripts/utils.js @@ -0,0 +1,25 @@ +export function getRandomInt(min, max, seed) { + return min + Math.floor((max - min + 1) * getRandomNumber(seed)); +} + +export function getRandomBoolean(seed) { + return getRandomNumber(seed) < 0.5; +} + +export function getRandomNumber(seed) { + const x = 1e4 * Math.sin(7 * seed); + return x - Math.floor(x); +} + +export function getHCF(a, b) { + return b == 0 ? a : getHCF(b, a % b); +} + +export function formatNumber(x, showPlus, showOne, showZero, suffix='') { + if (!showZero && x == 0) { return ''; } + plus = showPlus ? '+' : ''; + sign = x < 0 ? '-' : plus; + x = Math.abs(x); + showOne || x == 1 && (x = ''); + return sign + x + suffix; +} diff --git a/src/styles/index.css b/src/styles/index.css @@ -3,7 +3,7 @@ html { display: table; margin: 0 auto; - background-color: green; + background-color: #008080; } body { @@ -11,33 +11,11 @@ body { vertical-align: middle; width: 100vw; max-width: 800px; - background-color: white; - padding: 0 2em 0 1em; + background-color: #202020; + color: #c5c5c5; + padding: 0 1em; } -.QListOuter { - column-width: 200px; - column-gap: 4em; -} - -li { - break-inside: avoid-column; -} - -.QListInner { - list-style-type: lower-latin; -} - -.ButtonPanel { - margin-bottom: 1em; -} - -.Answer { +.QuestionSet { display: flex; - justify-content: flex-end; - color: green; -} - -.AnswerInvisible { - visibility: hidden; } diff --git a/www/index.html b/www/index.html @@ -14,7 +14,7 @@ </section> <section> <h2>Random questions</h2> - <div id="quadratic-equation-question-set"></div> + <div id="test-question-panel"></div> </section> <footer> <p>Fin</p>