commit ee9bacf6ee05131fa3d2554124331764965e3b82 parent f94d70aeb42520aa6677766e72f775bb76e0d38e Author: fen123456 <fenwthompson@gmail.com> Date: Mon, 3 Aug 2026 14:18:45 +0100 react router restructure Diffstat:
40 files changed, 589 insertions(+), 346 deletions(-)
diff --git a/.dockerignore b/.dockerignore @@ -0,0 +1,4 @@ +.react-router +build +node_modules +README.md +\ No newline at end of file diff --git a/.gitignore b/.gitignore @@ -1,6 +1,8 @@ -node_modules +.DS_Store +.env +/node_modules/ package-lock.json -www/js -www/css -www/*.woff -www/*.woff2 + +# React Router +/.react-router/ +/build/ diff --git a/Dockerfile b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:24-alpine AS development-dependencies-env +COPY . /app +WORKDIR /app +RUN npm ci + +FROM node:24-alpine AS production-dependencies-env +COPY ./package.json package-lock.json /app/ +WORKDIR /app +RUN npm ci --omit=dev + +FROM node:24-alpine AS build-env +COPY . /app/ +COPY --from=development-dependencies-env /app/node_modules /app/node_modules +WORKDIR /app +RUN npm run build + +FROM node:24-alpine +COPY ./package.json package-lock.json /app/ +COPY --from=production-dependencies-env /app/node_modules /app/node_modules +COPY --from=build-env /app/build /app/build +WORKDIR /app +CMD ["npm", "run", "start"] diff --git a/README.md b/README.md @@ -0,0 +1,87 @@ +# Welcome to React Router! + +A modern, production-ready template for building full-stack React applications using React Router. + +[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default) + +## Features + +- 🚀 Server-side rendering +- ⚡️ Hot Module Replacement (HMR) +- 📦 Asset bundling and optimization +- 🔄 Data loading and mutations +- 🔒 TypeScript by default +- 🎉 TailwindCSS for styling +- 📖 [React Router docs](https://reactrouter.com/) + +## Getting Started + +### Installation + +Install the dependencies: + +```bash +npm install +``` + +### Development + +Start the development server with HMR: + +```bash +npm run dev +``` + +Your application will be available at `http://localhost:5173`. + +## Building for Production + +Create a production build: + +```bash +npm run build +``` + +## Deployment + +### Docker Deployment + +To build and run using Docker: + +```bash +docker build -t my-app . + +# Run the container +docker run -p 3000:3000 my-app +``` + +The containerized application can be deployed to any platform that supports Docker, including: + +- AWS ECS +- Google Cloud Run +- Azure Container Apps +- Digital Ocean App Platform +- Fly.io +- Railway + +### DIY Deployment + +If you're familiar with deploying Node applications, the built-in app server is production-ready. + +Make sure to deploy the output of `npm run build` + +``` +├── package.json +├── package-lock.json (or pnpm-lock.yaml, or bun.lockb) +├── build/ +│ ├── client/ # Static assets +│ └── server/ # Server-side code +``` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. + +--- + +Built with ❤️ using React Router. diff --git a/src/components/Equation.jsx b/app/components/Equation.jsx diff --git a/app/components/MultipleSelection.jsx b/app/components/MultipleSelection.jsx @@ -0,0 +1,26 @@ +import * as React from 'react'; + +export default function MultipleSelection({ variable, setVariable, id, legend, values, labels = values }) { + const checked = values.map(value => variable.includes(value)); + + function updateVariable(e, index) { + const newChecked = checked.map((value, i) => i === index ? e.target.checked : value); + const validChecked = newChecked.some(Boolean) ? newChecked : checked.map((value) => !value); + setVariable(values.filter((_, i) => validChecked[i])); + } + + return ( + <fieldset key={id}> + <legend>{legend}</legend> + {values.map((value, i) => { + const name = `${id}:${value}`; + return ( + <div key={value} className="field-row"> + <input type="checkbox" id={name} name={name} checked={checked[i]} onChange={e => updateVariable(e, i)} /> + <label htmlFor={name}>{labels[i]}</label> + </div> + ); + })} + </fieldset> + ); +} diff --git a/src/components/Question.jsx b/app/components/Question.jsx diff --git a/src/components/QuestionList.jsx b/app/components/QuestionList.jsx diff --git a/app/components/QuestionWindow.jsx b/app/components/QuestionWindow.jsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import QuestionList from './QuestionList.jsx'; +import { getRandomInt, getRandomNumber, getRandomChoice } from '../scripts/utils.js'; + +import MultipleSelection from './MultipleSelection'; + +import AddingFractionsQuestion from './questions/AddingFractionsQuestion.jsx'; +import QuadraticEquationQuestion from './questions/QuadraticEquationQuestion.jsx'; +import Window from './Window.jsx'; + +function chooseInputComponent(type) { + switch (type) { + case 'multiple-selection': + return MultipleSelection; + } +} + +function chooseQuestionComponent(type) { + switch (type) { + case 'quadratic-equation': + return QuadraticEquationQuestion; + case 'adding-fractions': + return AddingFractionsQuestion; + } +} + +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 = chooseQuestionComponent(question.type); + const parameters = resolveParameters(question, variables); + return <QuestionComponent key={i} seed={seed+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 defaultVariables = Object.fromEntries( + data.variables.map(input => [input.id, input.default ?? input.values]) + ); + + const [showAnswers, setShowAnswers] = React.useState(false); + const [seed , setSeed ] = React.useState(Math.random()); + const [variables , setVariables ] = React.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 += n; + return randomQuestionList; + }); + + const fields = data.variables.map(input => { + const { type: type, ...props } = input; + const InputComponent = chooseInputComponent(type); + return <InputComponent key={input.id} variable={variables[input.id]} setVariable={setVariable(input.id)} {...props} />; + }); + + return ( + <Window title="Questions"> + {fields} + <fieldset> + <legend>Options</legend> + <button onClick={revealAnswers}> + {showAnswers ? "Hide" : "Show"} Answers + </button> + <button onClick={triggerRefresh}> + Refresh Questions + </button> + </fieldset> + <div className="field-border question-field kern"> + <ol className="master-question-list"> + {questions} + </ol> + </div> + </Window> + ); +} diff --git a/src/components/TestQuestionWindow.jsx b/app/components/TestQuestionWindow.jsx diff --git a/src/components/Window.jsx b/app/components/Window.jsx diff --git a/src/components/questions/AddingFractionsQuestion.jsx b/app/components/questions/AddingFractionsQuestion.jsx diff --git a/app/components/questions/QuadraticEquationQuestion.jsx b/app/components/questions/QuadraticEquationQuestion.jsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import Question from '../Question.jsx'; +import { getRandomInt, getRandomBoolean, getRandomChoice, 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: false}); + [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, aPlurality, framing }) { + const aChoices = Array(); + aPlurality.includes("a=1") && aChoices.push(1); + aPlurality.includes("a>1") && aChoices.push(2, 3); + const a = getRandomChoice(aChoices, seed); + + const factoriseChoices = Array(); + framing.includes("Solve") && factoriseChoices.push(false); + framing.includes("Factorise") && factoriseChoices.push(true); + const factorise = getRandomChoice(factoriseChoices, seed + .1); + + const [root1, root2] = getRandomRoots(seed + .2); + + const b = a * -(root1 + root2); + const c = a * root1 * root2; + + const quadratic = formatQuadraticExpression(a, b, c); + const 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-window.js b/app/data/test-question-window.js diff --git a/app/root.tsx b/app/root.tsx @@ -0,0 +1,81 @@ +import { + isRouteErrorResponse, + Links, + Meta, + NavLink, + Outlet, + Scripts, + ScrollRestoration, +} from "react-router"; + +import type { Route } from "./+types/root"; +import "./styles/index.css"; + +import Window from "./components/Window.jsx"; + +// for like googy fonts and stuff? +export const links: Route.LinksFunction = () => [ + { + rel: "stylesheet", + href: "https://unpkg.com/98.css" + } +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + <html lang="en"> + <head> + <meta charSet="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <Meta /> + <Links /> + </head> + <body> + {children} + <ScrollRestoration /> + <Scripts /> + </body> + </html> + ); +} + +export default function App() { + return ( + <> + <nav> + <NavLink to='/' exact className='nav-element' activeClassName="active">Home</NavLink> + <NavLink to='/questions' className='nav-element' activeClassName="active">Questions</NavLink> + </nav> + <Outlet /> + </> + ); +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = "Oops!"; + let details = "An unexpected error occurred."; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? "404" : "Error"; + details = + error.status === 404 + ? "The requested page could not be found." + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( + <Window title="404 Error! (probably)"> + <h1>{message}</h1> + <p>{details}</p> + {stack && ( + <pre className="w-full p-4 overflow-x-auto"> + <code>{stack}</code> + </pre> + )} + </Window> + ); +} diff --git a/app/routes.ts b/app/routes.ts @@ -0,0 +1,6 @@ +import { type RouteConfig, index, route } from "@react-router/dev/routes"; + +export default [ + index("routes/home.tsx"), + route("questions", "./routes/Questions.tsx") +] satisfies RouteConfig; diff --git a/app/routes/Home.tsx b/app/routes/Home.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import Window from '../components/Window.jsx'; +import type { Route } from "./+types/home"; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "TTMaths" }, + { name: "description", content: "Home Page" }, + ]; +} + + +export default function Home() { + return <Window title="poopwindow">poop butt</Window>; +} diff --git a/app/routes/Questions.tsx b/app/routes/Questions.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import type { Route } from "./+types/home"; +import TestQuestionWindow from '../components/TestQuestionWindow.jsx'; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "qestion page :)" }, + { name: "description", content: "Nor sure" }, + ]; +} + +export default function Questions() { + return <TestQuestionWindow />; +} diff --git a/src/scripts/auto-maths.js b/app/scripts/auto-maths.js diff --git a/app/scripts/utils.js b/app/scripts/utils.js @@ -0,0 +1,30 @@ +export function getRandomInt(min, max, seed) { + return min + Math.floor((max - min + 1) * getRandomNumber(seed)); +} + +export function getRandomChoice(arr, seed) { + return arr[getRandomInt(0, arr.length - 1, 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, parameters = {showPlus: false, showOne: true, showZero: true, suffix: ''}) { + if (!parameters.showZero && x == 0) { return ''; } + const plus = parameters.showPlus ? '+' : ''; + const sign = x < 0 ? '-' : plus; + x = Math.abs(x); + parameters.showOne || x == 1 && (x = ''); + parameters.suffix || (parameters.suffix = '') // if suffix undefined + return sign + x + parameters.suffix; +} diff --git a/app/styles/index.css b/app/styles/index.css @@ -0,0 +1,3 @@ +@import 'kern-typ/kern.css'; +@import './main.css'; +/*98.css is imported in root.tsx now*/ +\ No newline at end of file diff --git a/app/styles/main.css b/app/styles/main.css @@ -0,0 +1,45 @@ +body { + background-color: #008080; + margin: 0; +} + +nav { + width: 100vw; + border-bottom: 2px solid black; + background-color: #C0C0C0; + margin-bottom: 10px; +} +.nav-element { + padding: 3px 8px; + width: max-content; + min-width: 100px; + font-weight: bold; + display: inline-block; + margin: 5px; +} + +.window { + min-width: min-content; + max-width: max-content; +} + +fieldset { + margin-bottom: 7px; +} + +.field-border { + font-size: 1.25rem; + padding: 20px; +} + +.master-question-list { + margin: 0; + padding-left: 20px; + display: flex; + flex-wrap: wrap; + gap: 40px; +} + +.question-list { + list-style-type: lower-latin; +} diff --git a/build.mjs b/build.mjs @@ -1,7 +0,0 @@ -import * as esbuild from 'esbuild' -import config from './esbuild.json' with { type: "json" }; - -config.define = { IS_PRODUCTION: 'true' }; -config.minify = true; - -await esbuild.build(config); diff --git a/esbuild.json b/esbuild.json @@ -1,12 +0,0 @@ -{ - "entryPoints": [ - { "out": "js/index", "in": "src/scripts/index.js" }, - { "out": "css/index", "in": "src/styles/index.css" } - ], - "bundle": true, - "outdir": "www", - "loader": { - ".woff": "file", - ".woff2": "file" - } -} diff --git a/package.json b/package.json @@ -1,14 +1,31 @@ { + "name": "react-template", + "private": true, + "type": "module", + "scripts": { + "build": "react-router build", + "dev": "react-router dev", + "start": "react-router-serve ./build/server/index.js", + "typecheck": "react-router typegen && tsc" + }, "dependencies": { + "@react-router/node": "^8", + "@react-router/serve": "^8", "98.css": "^0.1.21", + "isbot": "^5.1.36", "kern-typ": "^0.2.0", - "react": "^19.2.8", - "react-dom": "^19.2.8" + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router": "^8" }, "devDependencies": { - "esbuild": "0.28.1" - }, - "allowScripts": { - "esbuild": false + "@react-router/dev": "^8", + "@tailwindcss/vite": "^4.2.2", + "@types/node": "^22", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "tailwindcss": "^4.2.2", + "typescript": "^5.9.3", + "vite": "^8.0.3" } } diff --git a/public/favicon.ico b/public/favicon.ico Binary files differ. diff --git a/react-router.config.ts b/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; + +export default { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, +} satisfies Config; diff --git a/serve.mjs b/serve.mjs @@ -1,13 +0,0 @@ -import * as esbuild from 'esbuild'; -import config from './esbuild.json' with { type: "json" }; - -config.define = { IS_PRODUCTION: 'false' }; -config.sourcemap = true; - -let ctx = await esbuild.context(config); - -await ctx.watch(); - -let { hosts, port } = await ctx.serve({ - servedir: 'www', -}); diff --git a/src/components/App.jsx b/src/components/App.jsx @@ -1,6 +0,0 @@ -import * as React from 'react'; -import TestQuestionWindow from './TestQuestionWindow.jsx'; - -export default function App() { - return <TestQuestionWindow />; -} diff --git a/src/components/MultipleSelection.jsx b/src/components/MultipleSelection.jsx @@ -1,26 +0,0 @@ -import * as React from 'react'; - -export default function MultipleSelection({ variable, setVariable, id, legend, values, labels = values }) { - checked = values.map(value => variable.includes(value)); - - function updateVariable(e, index) { - const newChecked = checked.map((value, i) => i === index ? e.target.checked : value); - const validChecked = newChecked.some(Boolean) ? newChecked : checked.map((value) => !value); - setVariable(values.filter((_, i) => validChecked[i])); - } - - return ( - <fieldset key={id}> - <legend>{legend}</legend> - {values.map((value, i) => { - const name = `${id}:${value}`; - return ( - <div key={value} className="field-row"> - <input type="checkbox" id={name} name={name} checked={checked[i]} onChange={e => updateVariable(e, i)} /> - <label htmlFor={name}>{labels[i]}</label> - </div> - ); - })} - </fieldset> - ); -} diff --git a/src/components/QuestionWindow.jsx b/src/components/QuestionWindow.jsx @@ -1,112 +0,0 @@ -import * as React from 'react'; -import QuestionList from './QuestionList.jsx'; -import { getRandomInt, getRandomNumber, getRandomChoice } from '../scripts/utils.js'; - -import MultipleSelection from './MultipleSelection'; - -import AddingFractionsQuestion from './questions/AddingFractionsQuestion.jsx'; -import QuadraticEquationQuestion from './questions/QuadraticEquationQuestion.jsx'; -import Window from './Window.jsx'; - -function chooseInputComponent(type) { - switch (type) { - case 'multiple-selection': - return MultipleSelection; - } -} - -function chooseQuestionComponent(type) { - switch (type) { - case 'quadratic-equation': - return QuadraticEquationQuestion; - case 'adding-fractions': - return AddingFractionsQuestion; - } -} - -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 = chooseQuestionComponent(question.type); - const parameters = resolveParameters(question, variables); - return <QuestionComponent key={i} seed={seed+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 }) { - - defaultVariables = Object.fromEntries( - data.variables.map(input => [input.id, input.default ?? input.values]) - ); - - const [showAnswers, setShowAnswers] = React.useState(false); - const [seed , setSeed ] = React.useState(Math.random()); - const [variables , setVariables ] = React.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 += n; - return randomQuestionList; - }); - - const fields = data.variables.map(input => { - const { type: type, ...props } = input; - const InputComponent = chooseInputComponent(type); - return <InputComponent key={input.id} variable={variables[input.id]} setVariable={setVariable(input.id)} {...props} />; - }); - - return ( - <Window title="Questions"> - {fields} - <fieldset> - <legend>Options</legend> - <button onClick={revealAnswers}> - {showAnswers ? "Hide" : "Show"} Answers - </button> - <button onClick={triggerRefresh}> - Refresh Questions - </button> - </fieldset> - <div className="field-border question-field kern"> - <ol className="master-question-list"> - {questions} - </ol> - </div> - </Window> - ); -} diff --git a/src/components/questions/QuadraticEquationQuestion.jsx b/src/components/questions/QuadraticEquationQuestion.jsx @@ -1,69 +0,0 @@ -import * as React from 'react'; -import Question from '../Question.jsx'; -import { getRandomInt, getRandomBoolean, getRandomChoice, 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, aPlurality, framing }) { - const aChoices = Array(); - aPlurality.includes("a=1") && aChoices.push(1); - aPlurality.includes("a>1") && aChoices.push(2, 3); - const a = getRandomChoice(aChoices, seed); - - const factoriseChoices = Array(); - framing.includes("Solve") && factoriseChoices.push(false); - framing.includes("Factorise") && factoriseChoices.push(true); - const factorise = getRandomChoice(factoriseChoices, seed + .1); - - 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/scripts/index.js b/src/scripts/index.js @@ -1,6 +0,0 @@ -import './auto-maths.js'; -import './react-roots.js'; - -if (!IS_PRODUCTION) { - new EventSource('/esbuild').addEventListener('change', () => location.reload()) -} diff --git a/src/scripts/react-roots.js b/src/scripts/react-roots.js @@ -1,12 +0,0 @@ -import { createElement } from 'react'; -import { createRoot } from 'react-dom/client'; - -import App from '../components/App.jsx'; - -function renderComponent(id, component) { - node = document.getElementById(id); - const root = createRoot(node); - root.render(createElement(component)); -} - -renderComponent('app', App); diff --git a/src/scripts/utils.js b/src/scripts/utils.js @@ -1,29 +0,0 @@ -export function getRandomInt(min, max, seed) { - return min + Math.floor((max - min + 1) * getRandomNumber(seed)); -} - -export function getRandomChoice(arr, seed) { - return arr[getRandomInt(0, arr.length - 1, 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 @@ -1,3 +0,0 @@ -@import 'kern-typ/kern.css'; -@import '98.css'; -@import './main.css'; diff --git a/src/styles/main.css b/src/styles/main.css @@ -1,29 +0,0 @@ -body { - background-color: #008080; -} - -.window { - min-width: min-content; - max-width: max-content; -} - -fieldset { - margin-bottom: 7px; -} - -.field-border { - font-size: 1.25rem; - padding: 20px; -} - -.master-question-list { - margin: 0; - padding-left: 20px; - display: flex; - flex-wrap: wrap; - gap: 40px; -} - -.question-list { - list-style-type: lower-latin; -} diff --git a/tsconfig.json b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "include": [ + "**/*", + "**/.server/**/*", + "**/.client/**/*", + ".react-router/types/**/*" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "paths": { + "~/*": ["./app/*"] + }, + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/vite.config.ts b/vite.config.ts @@ -0,0 +1,10 @@ +import { reactRouter } from "@react-router/dev/vite"; +import tailwindcss from "@tailwindcss/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [tailwindcss(), reactRouter()], + resolve: { + tsconfigPaths: true, + }, +}); diff --git a/www/index.html b/www/index.html @@ -1,11 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8"> - <link rel="stylesheet" href="css/index.css" /> - </head> - <body> - <div id="app"></div> - <script src="js/index.js"></script> - </body> -</html>