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