commit d1edbcd5905310dc71c9f3b384b071c12bf87648
parent 6c30d8c54fd6f495f02267574eb88b15d8352c7f
Author: hrryt <harry@mayesfield.uk>
Date: Wed, 23 Sep 2026 19:23:39 +0100
Revamp style
Diffstat:
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;
}