commit 16752f42e1aa7bbf5938e7e3b119d273c9f89942
Author: hrryt <harry@mayesfield.uk>
Date: Tue, 28 Jul 2026 13:32:30 +0100
Initial commit
Diffstat:
15 files changed, 298 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+package-lock.json
+www/js
+www/css
diff --git a/build.mjs b/build.mjs
@@ -0,0 +1,15 @@
+import * as esbuild from 'esbuild'
+
+await esbuild.build({
+ entryPoints: ['src/scripts/index.js'],
+ bundle: true,
+ minify: true,
+ sourcemap: true,
+ outdir: 'www/js',
+})
+
+await esbuild.build({
+ entryPoints: ['src/styles/index.css'],
+ bundle: true,
+ outdir: 'www/css',
+})
diff --git a/package.json b/package.json
@@ -0,0 +1,13 @@
+{
+ "dependencies": {
+ "kern-typ": "^0.2.0",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8"
+ },
+ "devDependencies": {
+ "esbuild": "0.28.1"
+ },
+ "allowScripts": {
+ "esbuild": false
+ }
+}
diff --git a/src/components/Equation.jsx b/src/components/Equation.jsx
@@ -0,0 +1,12 @@
+import * as React from 'react';
+import { render } from 'kern-typ';
+
+export default function Equation({ equation, displayMode = false }) {
+ const elementRef = React.useRef();
+
+ React.useEffect(() => {
+ render(equation, elementRef.current, { displayMode: displayMode });
+ }, [equation]);
+
+ return <div className="Equation" ref={elementRef} />;
+}
diff --git a/src/components/QItem.jsx b/src/components/QItem.jsx
@@ -0,0 +1,21 @@
+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
@@ -0,0 +1,41 @@
+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
@@ -0,0 +1,41 @@
+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
@@ -0,0 +1,43 @@
+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
@@ -0,0 +1,14 @@
+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/scripts/auto-maths.js b/src/scripts/auto-maths.js
@@ -0,0 +1,8 @@
+import renderMathInElement from 'kern-typ/auto-render';
+
+renderMathInElement(document.body, {
+ delimiters: [
+ { left: '$$', right: '$$', display: true },
+ { left: '$', right: '$', display: false },
+ ],
+});
diff --git a/src/scripts/contexts.js b/src/scripts/contexts.js
@@ -0,0 +1,5 @@
+import { createContext } from 'react';
+
+export const QListContext = createContext(false);
+export const RefreshContext = createContext(false);
+export const ShowAnswerContext = createContext(false);
diff --git a/src/scripts/index.js b/src/scripts/index.js
@@ -0,0 +1,2 @@
+import './auto-maths.js';
+import './react-roots.js';
diff --git a/src/scripts/react-roots.js b/src/scripts/react-roots.js
@@ -0,0 +1,12 @@
+import { createElement } from 'react';
+import { createRoot } from 'react-dom/client';
+
+import QuadraticEquationQuestionSet from '../components/QuadraticEquationQuestionSet.jsx';
+
+function renderComponent(id, component) {
+ node = document.getElementById(id);
+ const root = createRoot(node);
+ root.render(createElement(component));
+}
+
+renderComponent('quadratic-equation-question-set', QuadraticEquationQuestionSet);
diff --git a/src/styles/index.css b/src/styles/index.css
@@ -0,0 +1,43 @@
+@import 'kern-typ/kern.css';
+
+html {
+ display: table;
+ margin: 0 auto;
+ background-color: green;
+}
+
+body {
+ display: table-cell;
+ vertical-align: middle;
+ width: 100vw;
+ max-width: 800px;
+ background-color: white;
+ padding: 0 2em 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 {
+ display: flex;
+ justify-content: flex-end;
+ color: green;
+}
+
+.AnswerInvisible {
+ visibility: hidden;
+}
diff --git a/www/index.html b/www/index.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <link rel="stylesheet" href="css/index.css" />
+ </head>
+ <body>
+ <h1>Hello world!</h1>
+ <section>
+ <h2>Introduction</h2>
+ <p>$ x^2 + 5x + 6 $ is an expression.</p>
+ $$ x^2 + 5x + 6 = 0 $$
+ $$ x = -3 "or" x = -2 $$
+ </section>
+ <section>
+ <h2>Random questions</h2>
+ <div id="quadratic-equation-question-set"></div>
+ </section>
+ <footer>
+ <p>Fin</p>
+ </footer>
+ <script src="js/index.js"></script>
+ </body>
+</html>