website

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

commit d1edbcd5905310dc71c9f3b384b071c12bf87648
parent 6c30d8c54fd6f495f02267574eb88b15d8352c7f
Author: hrryt <harry@mayesfield.uk>
Date:   Wed, 23 Sep 2026 19:23:39 +0100

Revamp style

Diffstat:
Rstagit.png -> icon.png | 0
Mpublic/favicon.ico | 0
Mpush.sh | 4++--
Asrc/components/Card.jsx | 8++++++++
Dsrc/components/Header.jsx | 29-----------------------------
Asrc/components/QuestionCard.jsx | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/components/QuestionWindow.jsx | 94-------------------------------------------------------------------------------
Dsrc/components/Window.jsx | 17-----------------
Rsrc/data/questions/GCSE/quadratic-equations.js -> src/data/questions/Algebra/quadratic-equations.js | 0
Rsrc/data/questions/GCSE/double-transformations.js -> src/data/questions/Geometry and Measures/double-transformations.js | 0
Rsrc/data/questions/GCSE/adding-fractions.js -> src/data/questions/Number/adding-fractions.js | 0
Msrc/index.jsx | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/pages/Home.jsx | 44++++++--------------------------------------
Dsrc/pages/KatexRenderer.jsx | 34----------------------------------
Asrc/pages/LatexRenderer.jsx | 32++++++++++++++++++++++++++++++++
Msrc/pages/NotFound.jsx | 12+++++-------
Dsrc/pages/QuestionIndex.jsx | 66------------------------------------------------------------------
Msrc/styles/index.css | 1-
Msrc/styles/main.css | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
19 files changed, 348 insertions(+), 329 deletions(-)

diff --git a/stagit.png b/icon.png Binary files differ. diff --git a/public/favicon.ico b/public/favicon.ico Binary files differ. diff --git a/push.sh b/push.sh @@ -7,8 +7,8 @@ cd stagit stagit .. cp log.html index.html cp ../stagit.css style.css -cp ../stagit.png logo.png -cp ../stagit.png favicon.png +cp ../icon.png logo.png +cp ../icon.png favicon.png cp -r ../../website.git website.git cd .. diff --git a/src/components/Card.jsx b/src/components/Card.jsx @@ -0,0 +1,8 @@ +export default function Card({ title, children }) { + return ( + <article> + <h2>{title}</h2> + {children} + </article> + ); +} diff --git a/src/components/Header.jsx b/src/components/Header.jsx @@ -1,29 +0,0 @@ -import { Link } from 'wouter-preact'; - -function getActiveClass(active) { - return active ? "active" : ""; -} - -export default function Header() { - return ( - <header> - <nav class="window taskbar"> - <Link href="/" className={getActiveClass}> - <button class="taskbar-element"> - Home - </button> - </Link> - <Link href="/questions" className={getActiveClass}> - <button class="taskbar-element"> - Questions - </button> - </Link> - <Link href="/renderer" className={getActiveClass}> - <button class="taskbar-element"> - KaTeX Renderer - </button> - </Link> - </nav> - </header> - ); -} diff --git a/src/components/QuestionCard.jsx b/src/components/QuestionCard.jsx @@ -0,0 +1,94 @@ +import { useState } from 'preact/hooks'; +import QuestionList from './QuestionList.jsx'; +import Card from './Card.jsx'; +import { getRandomChoice, getRandomNumber } from '../scripts/randomUtils.js'; + +const seedDiff = 100; + +function resolveParameters(question, variables) { + const parameters = question.constants ?? { }; + const variableKeys = question.variables ?? { }; + Object.keys(variableKeys).forEach((key) => { + parameters[key] = variables[variableKeys[key]]; + }) + return parameters; +} + +function renderQuestion(seed, showAnswer, variables) { + return (question, i) => { + const QuestionComponent = question.type; + const parameters = resolveParameters(question, variables); + return <QuestionComponent key={i} seed={seed+seedDiff*i} showAnswer={showAnswer} {...parameters} />; + }; +} + +function getQuestionList(id, questions, showAnswers, seed, variables) { + return ( + <QuestionList key={id}> + {questions.map(renderQuestion(seed, showAnswers, variables))} + </QuestionList> + ); +} + +function shuffleArray(arr, n, seed) { + return Array.from({ length: n }).map(() => getRandomChoice(arr, ++seed)); +} + +export default function QuestionCard({ data }) { + + const dataVariables = data.variables ?? []; + + const defaultVariables = Object.fromEntries( + dataVariables.map(input => [input.id, input.default ?? input.values]) + ); + + const [showAnswers, setShowAnswers] = useState(false); + const [seed , setSeed ] = useState(Math.random()); + const [variables , setVariables ] = useState(defaultVariables); + + function setVariable(id) { + return (value) => { + setVariables({ + ...variables, + [id]: value + }); + } + } + + function revealAnswers() { setShowAnswers(!showAnswers); } + function triggerRefresh() { setSeed(getRandomNumber(seed)); } + + let current_seed = seed; + const questions = data.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, variables); + current_seed += seedDiff * n; + return randomQuestionList; + }); + + const fields = dataVariables.map(input => { + const { type: InputComponent, ...props } = input; + return <InputComponent key={input.id} variable={variables[input.id]} setVariable={setVariable(input.id)} {...props} />; + }); + + return ( + <Card title={data.title}> + {fields} + <fieldset> + <legend>Options</legend> + <button onClick={revealAnswers}> + {showAnswers ? "Show Questions" : "Show Answers"} + </button> + <button onClick={triggerRefresh}> + Refresh Questions + </button> + </fieldset> + <div class="field-border"> + <ol class="master-question-list"> + {questions} + </ol> + </div> + </Card> + ); +} diff --git a/src/components/QuestionWindow.jsx b/src/components/QuestionWindow.jsx @@ -1,94 +0,0 @@ -import { useState } from 'preact/hooks'; -import QuestionList from './QuestionList.jsx'; -import Window from './Window.jsx'; -import { getRandomChoice, getRandomNumber } from '../scripts/randomUtils.js'; - -const seedDiff = 100; - -function resolveParameters(question, variables) { - const parameters = question.constants ?? { }; - const variableKeys = question.variables ?? { }; - Object.keys(variableKeys).forEach((key) => { - parameters[key] = variables[variableKeys[key]]; - }) - return parameters; -} - -function renderQuestion(seed, showAnswer, variables) { - return (question, i) => { - const QuestionComponent = question.type; - const parameters = resolveParameters(question, variables); - return <QuestionComponent key={i} seed={seed+seedDiff*i} showAnswer={showAnswer} {...parameters} />; - }; -} - -function getQuestionList(id, questions, showAnswers, seed, variables) { - return ( - <QuestionList key={id}> - {questions.map(renderQuestion(seed, showAnswers, variables))} - </QuestionList> - ); -} - -function shuffleArray(arr, n, seed) { - return Array.from({ length: n }).map(() => getRandomChoice(arr, ++seed)); -} - -export default function QuestionWindow({ data }) { - - const dataVariables = data.variables ?? []; - - const defaultVariables = Object.fromEntries( - dataVariables.map(input => [input.id, input.default ?? input.values]) - ); - - const [showAnswers, setShowAnswers] = useState(false); - const [seed , setSeed ] = useState(Math.random()); - const [variables , setVariables ] = useState(defaultVariables); - - function setVariable(id) { - return (value) => { - setVariables({ - ...variables, - [id]: value - }); - } - } - - function revealAnswers() { setShowAnswers(!showAnswers); } - function triggerRefresh() { setSeed(getRandomNumber(seed)); } - - let current_seed = seed; - const questions = data.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, variables); - current_seed += seedDiff * n; - return randomQuestionList; - }); - - const fields = dataVariables.map(input => { - const { type: InputComponent, ...props } = input; - return <InputComponent key={input.id} variable={variables[input.id]} setVariable={setVariable(input.id)} {...props} />; - }); - - return ( - <Window title={data.title}> - {fields} - <fieldset> - <legend>Options</legend> - <button onClick={revealAnswers}> - {showAnswers ? "Show Questions" : "Show Answers"} - </button> - <button onClick={triggerRefresh}> - Refresh Questions - </button> - </fieldset> - <div class="field-border"> - <ol class="master-question-list"> - {questions} - </ol> - </div> - </Window> - ); -} diff --git a/src/components/Window.jsx b/src/components/Window.jsx @@ -1,17 +0,0 @@ -export default function Window({ children, title, maximised }){ - return ( - <div class="window"> - <div class="title-bar"> - <div class="title-bar-text">{title}</div> - <div class="title-bar-controls"> - <button class="window-button" aria-label="Minimize" /> - <button class="window-button" aria-label={maximised ? "Restore" : "Maximize"} /> - <button class="window-button" aria-label="Close" /> - </div> - </div> - <div class="window-body"> - {children} - </div> - </div> - ) -} diff --git a/src/data/questions/GCSE/quadratic-equations.js b/src/data/questions/Algebra/quadratic-equations.js diff --git a/src/data/questions/GCSE/double-transformations.js b/src/data/questions/Geometry and Measures/double-transformations.js diff --git a/src/data/questions/GCSE/adding-fractions.js b/src/data/questions/Number/adding-fractions.js diff --git a/src/index.jsx b/src/index.jsx @@ -1,26 +1,121 @@ import { render } from 'preact'; -import { Route, Switch } from 'wouter-preact'; - -import Header from './components/Header.jsx'; +import { Route, Switch, Link } from 'wouter-preact'; +import QuestionCard from './components/QuestionCard.jsx'; +import Card from './components/Card.jsx'; import Home from './pages/Home.jsx'; -import QuestionIndex from './pages/QuestionIndex.jsx'; -import KatexRenderer from './pages/KatexRenderer.jsx'; +import LatexRenderer from './pages/LatexRenderer.jsx'; import NotFound from './pages/NotFound.jsx'; import './styles/index.css'; +const data = import.meta.glob('./*/*.js', { + eager: true, import: 'data', base: './data/questions' +}); + +function getHref(path) { + const relative = path.match(/([^\/]+).js$/)[1]; + return `/questions/${relative}`; +} + +const tree = { }; +for (const path in data) { + const category = path.match(/^\.\/(.+)\//)[1]; + tree[category] ??= []; + tree[category].push({ + href: getHref(path), + title: data[path].title + }); +} + +function getActiveClass(active) { + return active ? "active" : ""; +} + +function SidebarLink({ href, title, children }) { + return ( + <li> + <Link href={href} className={getActiveClass}> + {title} + </Link> + {children} + </li> + ); +} + +function SidebarQuestionCategory({ categoryName, categoryList }) { + const questionLinks = categoryList.map(c => <SidebarLink href={c.href} title={c.title} />); + return ( + <li> + <details open> + <summary>{categoryName}</summary> + <ul> + {questionLinks} + </ul> + </details> + </li> + ); +} + +const sidebarQuestions = Object.keys(tree).map(categoryName => { + return <SidebarQuestionCategory categoryName={categoryName} categoryList={tree[categoryName]} />; +}); + +const questionRoutes = Object.keys(data).map(path => { + return <Route path={getHref(path)}><QuestionCard data={data[path]} /></Route>; +}) + +function Sidebar() { + return ( + <nav id="sidebar"> + <ul> + <SidebarLink href="/" title="Home" /> + <SidebarLink href="/questions" title="Questions"> + <Route path="/questions/*?"> + <ul> + {sidebarQuestions} + </ul> + </Route> + </SidebarLink> + <SidebarLink href="/renderer" title="LaTeX Renderer" /> + </ul> + </nav> + ); +} + +function QuestionIndex() { + return ( + <Card title="Questions"> + <nav class="index"> + <ul> + {sidebarQuestions} + </ul> + </nav> + </Card> + ); +} + export function App() { return ( <> - <Header /> - <Switch> - <Route path="/" component={Home} /> - <Route path="/questions" component={QuestionIndex} /> - <Route path="/questions/*" component={QuestionIndex} /> - <Route path="/renderer" component={KatexRenderer} /> - <Route component={NotFound} /> - </Switch> + <header> + <a id="logo" href="/"><h1>TTMaths</h1></a> + <span id="tagline">resources for learning</span> + </header> + <div id="sidebar-and-main"> + <Sidebar /> + <main> + <Switch> + <Route path="/" component={Home} /> + <Route path="/questions" component={QuestionIndex} /> + <Route path="/questions/*"> + {questionRoutes} + </Route> + <Route path="/renderer" component={LatexRenderer} /> + <Route component={NotFound} /> + </Switch> + </main> + </div> </> ); } diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx @@ -1,43 +1,11 @@ -import Window from '../components/Window.jsx'; -import Plot from '../components/plotting/Plot.jsx'; -import Quadratic from '../components/plotting/Quadratic.jsx'; -import Cubic from '../components/plotting/Cubic.jsx'; -import Polygon from '../components/plotting/Polygon.jsx'; -import Angle from '../components/plotting/Angle.jsx'; -import Segment from '../components/plotting/Segment.jsx'; -import Line from '../components/plotting/Line.jsx'; -import Point from '../components/plotting/Point.jsx'; -import Label from '../components/plotting/Label.jsx'; -import Sinusoid from '../components/plotting/Sinusoid.jsx'; +import Card from '../components/Card.jsx'; export default function Home() { - const [A, B, C] = [[1,4], [3,2], [2,1]]; return ( - <main> - <Window title="Home"> - <p>Hello World!</p> - <div class="field-border"> - <Plot viewBox={[-5, -4, 8, 9]}> - <Quadratic a={2} b={2} c={-2} colour="1" /> - <Polygon points={[[1,2], [-1,1], [-4,3]]} label="P" colour="2" /> - <Cubic a={2} b={1} c={-1} d={2} colour="3" /> - <Point point={[1,0]} label="Q" colour="3" /> - <Line line={{a: -10, b: -9, c: -1}} colour="4" /> - <Sinusoid period={5} amp={0.5} dx={-11} dy={-2} colour="5" /> - </Plot> - <Plot mode="blank" viewBox={[0, 0, 5, 5]}> - <Label point={A} position="top">A</Label> - <Label point={B} position="right">B</Label> - <Label point={C} position="bottom">C</Label> - <Segment points={[A, B]} marker="hash" /> - <Segment points={[C, B]} marker="double-feather" /> - <Segment points={[A, C]} /> - <Angle points={[A, B, C]} right={true} /> - <Angle points={[B, C, A]} /> - <Angle points={[C, A, B]} /> - </Plot> - </div> - </Window> - </main> + <Card title="Welcome to TTMaths!"> + <p> + Here you can find a range of useful teaching and study resources for mathematics, geared toward the national curriculum in England. + </p> + </Card> ); } diff --git a/src/pages/KatexRenderer.jsx b/src/pages/KatexRenderer.jsx @@ -1,34 +0,0 @@ -import { useEffect, useRef, useState } from 'preact/hooks'; -import Equation from '../components/Equation.jsx'; -import Window from '../components/Window.jsx' - -export default function KatexRenderer() { - const [text, setText] = useState("") - const textRef = useRef(null) - - function updateText(e) { - setText(e.target.value) - } - - useEffect(() => { - textRef.current.addEventListener('input', updateText) - }) - - return ( - <main> - <Window title="KaTeX Renderer"> - <p>Hello World!</p> - <textarea - class="field-border" - placeholder="LaTeX code here..." - value={text} - ref={textRef} - onChange={e => updateText(e)} - /> - <div class="field-border"> - <Equation equation={text} /> - </div> - </Window> - </main> - ) -} diff --git a/src/pages/LatexRenderer.jsx b/src/pages/LatexRenderer.jsx @@ -0,0 +1,32 @@ +import { useEffect, useRef, useState } from 'preact/hooks'; +import Equation from '../components/Equation.jsx'; +import Card from '../components/Card.jsx' + +export default function LatexRenderer() { + const [text, setText] = useState("") + const textRef = useRef(null) + + function updateText(e) { + setText(e.target.value) + } + + useEffect(() => { + textRef.current.addEventListener('input', updateText) + }) + + return ( + <Card title="LaTeX Renderer"> + <p>Render LaTeX code to the screen.</p> + <textarea + placeholder="LaTeX code here..." + value={text} + ref={textRef} + onChange={e => updateText(e)} + spellcheck={false} + /> + <div class="field-border"> + <Equation equation={text} /> + </div> + </Card> + ) +} diff --git a/src/pages/NotFound.jsx b/src/pages/NotFound.jsx @@ -1,11 +1,9 @@ -import Window from '../components/Window.jsx'; +import Card from '../components/Card.jsx'; export default function NotFound() { return ( - <main> - <Window title="Error"> - <img class="http-error" src="https://httpducks.com/404.jpg" /> - </Window> - </main> - ); + <Card title="Error"> + <img class="http-error" src="https://httpducks.com/404.jpg" /> + </Card> +); } diff --git a/src/pages/QuestionIndex.jsx b/src/pages/QuestionIndex.jsx @@ -1,66 +0,0 @@ -import { Route, Switch, Link } from 'wouter-preact'; -import QuestionWindow from '../components/QuestionWindow.jsx'; -const data = import.meta.glob('./*/*.js', { - eager: true, import: 'data', base: '../data/questions' -}); - -function getHref(path) { - const relative = path.match(/\.\/(.+).js/)[1]; - return `/questions/${relative}`; -} - -function QuestionLink({ href, title }) { - return ( - <li> - <Link href={href}> - {title} - </Link> - </li> - ); -} - -const tree = { }; -for (const path in data) { - const category = path.match(/^\.\/(.+)\//)[1]; - tree[category] ??= []; - tree[category].push( - <QuestionLink href={getHref(path)} title={data[path].title} /> - ); -} - -function Category({ category, questionLinks }) { - return ( - <li> - <details open> - <summary>{category}</summary> - <ul> - {questionLinks} - </ul> - </details> - </li> - ); -} - -const categories = Object.keys(tree).map(category => - <Category category={category} questionLinks={tree[category]} /> -); - -const routes = Object.keys(data).map(path => { - function Component() { return <QuestionWindow data={data[path]} />; } - return <Route path={getHref(path)} component={Component} />; -}) - -export default function QuestionIndex() { - return ( - <main> - <nav class="sidebar"> - <ul class="tree-view"> - {categories} - </ul> - </nav> - <Switch> - {routes} - </Switch> - </main> - ); -} diff --git a/src/styles/index.css b/src/styles/index.css @@ -1,4 +1,3 @@ @import 'katex/dist/katex.min.css'; -@import '98.css'; @import './main.css'; @import './svg.css'; diff --git a/src/styles/main.css b/src/styles/main.css @@ -1,9 +1,13 @@ :root { - --highlight: #008080; + --highlight: #d7834f; + --background: #2a2a2a; + --main-bg: #333333; + --main-border: #5e5e5a; + --font-color: #b3b1b1; + --light-font-color: #bdbbb0; + --nav-link-color: #979594; - --background: var(--highlight); - - --graph-1: var(--highlight); + --graph-1: #008080; --graph-2: #43509E; --graph-3: #DC267F; --graph-4: #FE6100; @@ -12,30 +16,106 @@ body { background-color: var(--background); - margin: 0; + color: var(--font-color); + font-family: Adelle,Roboto Slab,DejaVu Serif,Georgia,Times New Roman,sans-serif; + width: 940px; + margin: 0 auto; +} + +div#sidebar-and-main { + display: flex; +} + +main { + background-color: var(--main-bg); + border: solid 1px var(--main-border); + padding: 22px; + width: 670px; +} + +nav#sidebar { + font-size: 16px; + font-weight: 700; + line-height: 25px; + margin-right: 20px; + min-width: 250px; +} + +ul { + list-style: none; + padding-left: 25px; + margin-bottom: 1.4em; +} + +a { + text-decoration: none; + color: var(--highlight); +} + +nav#sidebar > ul { + padding-left: 0; +} + +nav#sidebar details { + font-size: 13px; + font-weight: 400; +} + +nav#sidebar a { + transition-duration: .3s; + transition-property: color; + background-color: initial; + color: var(--nav-link-color); +} + +nav#sidebar :is(a.active,a:hover) { + color: var(--highlight); } -nav.taskbar { - width: 100vw; +header { + padding: 1rem 0; + display: grid; + grid-template-areas: "logo tagline"; + grid-template-columns: auto 1fr; + align-items: center; } -a.active > button.taskbar-element { - font-weight: bold; - box-shadow: inset -1px -1px #ffffff, inset 1px 1px #0a0a0a, - inset -2px -2px #dfdfdf, inset 2px 2px #808080; +header #logo { + grid-area: logo; } -.window { - width: fit-content; +header #tagline { + grid-area: tagline; + margin-left: .8em; + font-size: clamp(.8rem,2vw,1.3rem); + color: var(--light-font-color); } fieldset { margin-bottom: 7px; + border: solid 1px var(--main-border); +} + +button { + background-color: var(--background); + border: solid 1px var(--main-border); + color: var(--font-color); +} + +textarea { + background-color: var(--background); + border: solid 1px var(--main-border); + color: var(--font-color); + width: 100%; } .field-border { font-size: large; padding: 20px; + margin-top: 20px; + background-color: white; + color: black; + border: solid 8px var(--highlight); } ol.master-question-list { @@ -51,25 +131,10 @@ ol.question-list { list-style-type: lower-latin; } -main { - padding: 8px; - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -nav.sidebar { - width: 200px; -} - button { margin-right: 3px; } -button.window-button { - margin-right: 0; -} - img.http-error { max-width: 400px; }