From f63c59f854076941efbf20acabbae466a1d3c7bf Mon Sep 17 00:00:00 2001 From: Wayne Duran Date: Sat, 5 Mar 2022 17:34:05 +0800 Subject: [PATCH] feat: new worksheet: numbers to words --- cypress/index.d.ts | 1 + cypress/integration/navigation.spec.ts | 33 ++++--- .../integration/worksheet_numbers_to_words.ts | 15 ++++ cypress/support/commands.js | 1 + src/components/AppWrapper.tsx | 2 + src/components/Blank.tsx | 17 +++- src/components/MultiPaperPage.tsx | 57 +++++++++---- src/components/ProblemList.tsx | 67 +++++++++++++++ src/components/ProblemListItem.tsx | 51 +++++++++++ src/components/forms/NumberRangeSlider.tsx | 67 +++++++++------ src/elements/PageTitle.tsx | 27 ++++++ src/lib/Range.ts | 6 ++ src/lib/numberToWords.spec.ts | 81 ++++++++++++++++++ src/lib/numberToWords.ts | 85 +++++++++++++++++++ .../AdditionSentence.tsx | 9 +- src/pages/additionFillTheBlanks/AftbData.ts | 5 +- .../additionFillTheBlanks/PreviewAftb.tsx | 73 ++-------------- src/pages/main/MainPage.tsx | 4 + .../CustomizeNumbersToWordsForm.tsx | 39 +++++++++ .../numbersToWords/NumbersToWordsData.tsx | 8 ++ .../numbersToWords/NumbersToWordsPage.tsx | 34 ++++++++ .../numbersToWords/PreviewNumbersToWords.tsx | 83 ++++++++++++++++++ src/pages/patterns/PreviewPatterns.tsx | 31 ++++--- src/pages/placeValues/PreviewPlaceValues.tsx | 49 +++-------- 24 files changed, 666 insertions(+), 179 deletions(-) create mode 100644 cypress/integration/worksheet_numbers_to_words.ts create mode 100644 src/components/ProblemList.tsx create mode 100644 src/components/ProblemListItem.tsx create mode 100644 src/elements/PageTitle.tsx create mode 100644 src/lib/Range.ts create mode 100644 src/lib/numberToWords.spec.ts create mode 100644 src/lib/numberToWords.ts create mode 100644 src/pages/numbersToWords/CustomizeNumbersToWordsForm.tsx create mode 100644 src/pages/numbersToWords/NumbersToWordsData.tsx create mode 100644 src/pages/numbersToWords/NumbersToWordsPage.tsx create mode 100644 src/pages/numbersToWords/PreviewNumbersToWords.tsx diff --git a/cypress/index.d.ts b/cypress/index.d.ts index eee2bf2..5e2b9e8 100644 --- a/cypress/index.d.ts +++ b/cypress/index.d.ts @@ -6,6 +6,7 @@ declare namespace Cypress { visitAdditionFillTheBlanks(): Chainable; visitWorksheetPatterns(): Chainable; visitWorksheetPlaceValues(): Chainable; + visitWorksheetNumbersToWords(): Chainable; } interface Chainable { diff --git a/cypress/integration/navigation.spec.ts b/cypress/integration/navigation.spec.ts index 6438487..e11b7d5 100644 --- a/cypress/integration/navigation.spec.ts +++ b/cypress/integration/navigation.spec.ts @@ -1,13 +1,17 @@ +function goBackHome() { + cy.findByRole('banner').within(() => { + cy.findByText('Printables').click(); + }); +} + it('can visit all subpages', () => { cy.visitHome(); cy.contains('Printables'); cy.findByRole('link', { name: /calendar/i }).click(); cy.findByRole('button', { name: /print calendar/i }); - // Back home - cy.findByRole('banner').within(() => { - cy.findByText('Printables').click(); - }); + goBackHome(); + cy.contains(/Printable Materials for Education/i); // Addition Worksheets @@ -19,10 +23,7 @@ it('can visit all subpages', () => { cy.findByRole('heading', { name: /addition.+fill.+blank/i }); }); - // Back home - cy.findByRole('banner').within(() => { - cy.findByText('Printables').click(); - }); + goBackHome(); // Pattern Worksheets cy.findByRole('list', { name: /worksheets/i }).within(() => { @@ -33,10 +34,7 @@ it('can visit all subpages', () => { cy.findByRole('heading', { name: /patterns/i }); }); - // Back home - cy.findByRole('banner').within(() => { - cy.findByText('Printables').click(); - }); + goBackHome(); // Place Value Worksheets cy.findByRole('list', { name: /worksheets/i }).within(() => { @@ -46,4 +44,15 @@ it('can visit all subpages', () => { cy.withinCustomizeForm(() => { cy.findByRole('heading', { name: /place values/i }); }); + + goBackHome(); + + // Numbers to Words + cy.findByRole('list', { name: /worksheets/i }).within(() => { + cy.findByRole('link', { name: /numbers to words/i }).click(); + }); + + cy.withinCustomizeForm(() => { + cy.findByRole('heading', { name: /numbers to words/i }); + }); }); diff --git a/cypress/integration/worksheet_numbers_to_words.ts b/cypress/integration/worksheet_numbers_to_words.ts new file mode 100644 index 0000000..fa89776 --- /dev/null +++ b/cypress/integration/worksheet_numbers_to_words.ts @@ -0,0 +1,15 @@ +it('can create numbers to words worksheet', () => { + cy.visitWorksheetNumbersToWords(); + + cy.findByLabelText(/number of problems/i).clearType('7'); + cy.setNumberRange('number-range-slider', 10, 99); + cy.withinPreview(() => { + cy.findByRole('list', { name: 'Problems' }).within((subject) => { + cy.wrap(subject).findAllByRole('listitem') + .should('have.length', 7) + .each(($li) => { + cy.wrap($li).contains(/[a-z\-\s]+:\s+_+/); + }); + }); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a654242..7d7bee6 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -32,6 +32,7 @@ const paths = { additionFillTheBlanks: '/addition-fill-the-blanks', worksheetPatterns: '/worksheet-patterns', worksheetPlaceValues: '/worksheet-place-values', + worksheetNumbersToWords: '/worksheet-numbers-to-words', }; const pathNames = Object.keys(paths); diff --git a/src/components/AppWrapper.tsx b/src/components/AppWrapper.tsx index 02dd544..0826e92 100644 --- a/src/components/AppWrapper.tsx +++ b/src/components/AppWrapper.tsx @@ -4,6 +4,7 @@ import { createTheme, makeStyles, MuiThemeProvider } from '@material-ui/core'; import BaseStyle from './BaseStyle'; import PrintablesAppBar from './PrintablesAppBar'; import ScrollToTop from './ScrollToTop'; +import NumbersToWordsPage from '../pages/numbersToWords/NumbersToWordsPage'; const MainPage = lazy(() => import('../pages/main/MainPage')); const CalendarPage = lazy(() => import('../pages/calendar/CalendarPage')); @@ -48,6 +49,7 @@ function AppWrapper(): JSX.Element { } /> } /> } /> + } /> diff --git a/src/components/Blank.tsx b/src/components/Blank.tsx index a8501b1..c0fa480 100644 --- a/src/components/Blank.tsx +++ b/src/components/Blank.tsx @@ -12,15 +12,24 @@ const styles = makeStyles(() => ({ minWidth: '1.6em', // 32px textAlign: 'center', + '&.problem-blank-wide': { + minWidth: '2.6em', + paddingLeft: '0.6em', + paddingRight: '0.6em', + }, + '& > .underline': { color: 'transparent', }, }, })); +type BlankWidth = 'short' | 'wide'; + export interface BlankProps { answer: string | number | Stringable; showAnswer: boolean; + width?: BlankWidth; } const underlines = (length: number): string => { @@ -31,10 +40,10 @@ const underlines = (length: number): string => { return str; }; -function Blank({ answer, showAnswer }: BlankProps): JSX.Element { +function Blank({ answer, showAnswer, width }: BlankProps): JSX.Element { const classes = styles(); return ( - + { showAnswer ? answer @@ -48,4 +57,8 @@ function Blank({ answer, showAnswer }: BlankProps): JSX.Element { ); } +Blank.defaultProps = { + width: 'short', +}; + export default Blank; diff --git a/src/components/MultiPaperPage.tsx b/src/components/MultiPaperPage.tsx index d1abda4..fcd0da7 100644 --- a/src/components/MultiPaperPage.tsx +++ b/src/components/MultiPaperPage.tsx @@ -13,8 +13,22 @@ export type Builder = (item: T, index: number, array: T[] | undefined) => JSX export type Props = Record; export type PropsCallback = (props: Props, options: PropsCallbackOptions) => Props; + +type WrapperWithPropsCallback = ElementType & { + propsCallback: PropsCallback; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isWrapperWithPropsCallback(obj: any): obj is WrapperWithPropsCallback { + return typeof obj === 'function' + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + && obj.propsCallback !== undefined + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + && typeof obj.propsCallback === 'function'; +} + interface WrapperBuilder { - wrapper?: ElementType | null; + wrapper?: WrapperWithPropsCallback | ElementType | null; wrapperProps?: Props; wrapperPropsCallback?: PropsCallback; data: T[]; @@ -38,12 +52,21 @@ function wrappedContent({ instanceIndex, memberIndex, }: WrapperBuilderArgs): ReactNode { if (wrapper !== null) { + let propsCallback = wrapperPropsCallback; + if (isWrapperWithPropsCallback(wrapper)) { + propsCallback = (props, options) => + // eslint-disable-next-line implicit-arrow-linebreak + wrapperPropsCallback( + wrapper.propsCallback(props, options), + options, + ); + } return createElement( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion wrapper!, { - ...wrapperPropsCallback( - wrapperProps || {}, + ...propsCallback( + wrapperProps ?? {}, { instanceIndex, memberIndex }, ), }, @@ -141,7 +164,7 @@ function MultiPaperPage({ } setAttemptsTofix(attemptsToFix + 1); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [dataPages, options]); let count = 0; @@ -153,23 +176,23 @@ function MultiPaperPage({ const rendered = ( - { index === 0 ? header : null } + {index === 0 ? header : null} { - wrappedContent({ - wrapper, - wrapperProps, - wrapperPropsCallback: wrapperPropsCallback || passThrough, - data: dataPage, - renderItems: builder, - instanceIndex: index, - memberIndex: count, - }) - } - { index === dataPages.length - 1 ? footer : null } + wrappedContent({ + wrapper, + wrapperProps, + wrapperPropsCallback: wrapperPropsCallback || passThrough, + data: dataPage, + renderItems: builder, + instanceIndex: index, + memberIndex: count, + }) + } + {index === dataPages.length - 1 ? footer : null} ); count += dataPage.length; diff --git a/src/components/ProblemList.tsx b/src/components/ProblemList.tsx new file mode 100644 index 0000000..f8d54ba --- /dev/null +++ b/src/components/ProblemList.tsx @@ -0,0 +1,67 @@ +import makeStyles from '@material-ui/core/styles/makeStyles'; +import React, { CSSProperties, ReactNode } from 'react'; +import { Props, PropsCallback } from './MultiPaperPage'; + +interface ProblemListProps { + children: ReactNode; + className?: string; + columns?: number; + style?: CSSProperties; + label?: string; +} + +const styles = makeStyles(() => ({ + // All em units equivalent are based on a 20px font size base + list: { + margin: '5mm 0 0 0', + padding: 0, + columnCount: 2, + columnWidth: 'auto', + counterReset: 'problem', + + '& > li': { + counterIncrement: 'problem', + }, + + '& > li::marker': { + content: 'counter(problem) "."', + }, + }, +})); + +function ProblemList({ + children, className, columns, style, label, +}: ProblemListProps): JSX.Element { + const classes = styles(); + const cols = columns ?? 1; + const paddedClassName = className ? ` ${className}` : ''; + const styleUsed = { ...style, columns: cols }; + + return ( +
    + {children} +
+ ); +} + +const propsCallback: PropsCallback = (inputProps: Props, { memberIndex }) => ({ + ...inputProps, + style: { + counterReset: `problem ${memberIndex}`, + }, +}); + +ProblemList.propsCallback = propsCallback; + +ProblemList.defaultProps = { + className: null, + columns: 1, + style: null, + label: null, +}; + +export default ProblemList; diff --git a/src/components/ProblemListItem.tsx b/src/components/ProblemListItem.tsx new file mode 100644 index 0000000..f9cd4d2 --- /dev/null +++ b/src/components/ProblemListItem.tsx @@ -0,0 +1,51 @@ +import { makeStyles } from '@material-ui/core'; +import React, { ReactNode } from 'react'; + +interface ProblemListItemProps { + children: ReactNode; + label?: string; + fontSize?: number; + className?: string; +} + +const defaultFontSize = 20; +const defaultLabel = 'Problem'; + +const styles = makeStyles(() => ({ + // All em units equivalent are based on a 20px font size base + li: { + padding: '1.15em 0 1.15em 1.15em', // '6mm 0 6mm 6mm', // 23px + marginLeft: '1.9em', // '10mm', // 38px + '-webkit-column-break-inside': 'avoid', + pabeBreakInside: 'avoid', + breakInside: 'avoid', + + '&::marker': { + fontSize: '0.8em', // 16px + }, + }, +})); + +function ProblemListItem({ + children, label, fontSize, className, +}: ProblemListItemProps): JSX.Element { + const classes = styles(); + const paddedClassName = className ? ` ${className}` : ''; + return ( +
  • + {children} +
  • + ); +} + +ProblemListItem.defaultProps = { + fontSize: defaultFontSize, + className: '', + label: defaultLabel, +}; + +export default ProblemListItem; diff --git a/src/components/forms/NumberRangeSlider.tsx b/src/components/forms/NumberRangeSlider.tsx index 4eab8c0..5e7efad 100644 --- a/src/components/forms/NumberRangeSlider.tsx +++ b/src/components/forms/NumberRangeSlider.tsx @@ -5,17 +5,26 @@ import { import React, { ChangeEvent, useEffect, useRef } from 'react'; import HtmlFieldChangeEvent from '../../lib/HtmlFieldChangeEvent'; import './NumberRangeSlider.css'; +import Range from '../../lib/Range'; + +const { round } = Math; const valueScale = (x: number): number => { if (x < 11) { return x; } - return (x - 10) * 10; + if (x < 21) { + return (x - 10) * 10; + } + return (x - 20) * 100; }; const valueDescale = (x: number): number => { + if (x > 100) { + return round((x / 100) + 20); + } if (x > 10) { - return Math.round((x / 10) + 10); + return round((x / 10) + 10); } return x; }; @@ -40,16 +49,12 @@ const useStyles = makeStyles(() => ({ }, })); -interface Range { - from: number; - to: number; -} - export type NumberRangeChangeCallback = (value: Range, event: HtmlFieldChangeEvent) => void; interface NumberRangeSliderProps extends Range { label: string; id: string; + magnitude?: number; onChange: NumberRangeChangeCallback; 'data-test'?: string; } @@ -62,7 +67,7 @@ type InputChangeHandlerBuilder = (callback: InputChangeCallback) => InputChangeH function NumberRangeSlider(options: NumberRangeSliderProps): JSX.Element { const classes = useStyles(); const { - from, to, label, onChange, id, + from, to, label, onChange, id, magnitude, } = options; const labelId = `${id}-label`; const changeHandler: ChangeHanlder = (value, event) => { @@ -132,6 +137,27 @@ function NumberRangeSlider(options: NumberRangeSliderProps): JSX.Element { }; }); + const marks = [ + { + value: 0, + label: '0', + }, + { + value: 10, + label: '10', + }, + { + value: 20, + label: '100', + }, + ]; + let max = 100; + if (magnitude === 3) { + marks.push({ value: 30, label: '1000' }); + max = 1000; + } + const maxSlider = marks[marks.length - 1]?.value ?? 20; + return (
    { changeHandler(value, event as HtmlFieldChangeEvent); @@ -221,4 +234,8 @@ function NumberRangeSlider(options: NumberRangeSliderProps): JSX.Element { ); } +NumberRangeSlider.defaultProps = { + magnitude: 2, +}; + export default NumberRangeSlider; diff --git a/src/elements/PageTitle.tsx b/src/elements/PageTitle.tsx new file mode 100644 index 0000000..68d612d --- /dev/null +++ b/src/elements/PageTitle.tsx @@ -0,0 +1,27 @@ +import makeStyles from '@material-ui/core/styles/makeStyles'; +import Typography from '@material-ui/core/Typography'; +import React, { ReactNode } from 'react'; + +const styles = makeStyles(() => ({ + heading: { + textAlign: 'center', + }, +})); +interface PageTitleProps { + children: ReactNode; +} + +function PageTitle({ children }: PageTitleProps): JSX.Element { + const classes = styles(); + return ( + + {children} + + ); +} + +export default PageTitle; diff --git a/src/lib/Range.ts b/src/lib/Range.ts new file mode 100644 index 0000000..3c210a2 --- /dev/null +++ b/src/lib/Range.ts @@ -0,0 +1,6 @@ +interface Range { + from: number; + to: number; +} + +export default Range; diff --git a/src/lib/numberToWords.spec.ts b/src/lib/numberToWords.spec.ts new file mode 100644 index 0000000..95dc207 --- /dev/null +++ b/src/lib/numberToWords.spec.ts @@ -0,0 +1,81 @@ +/* eslint-disable no-underscore-dangle */ +import numberToWords from './numberToWords'; + +interface TestData { + input: number; + expected: string; +} + +// eslint-disable-next-line @typescript-eslint/naming-convention +function _t(input: number, expected: string): TestData { + return { input, expected }; +} + +describe('numberToWords', () => { + const testData: Array = [ + _t(0, 'zero'), + _t(1, 'one'), + _t(2, 'two'), + _t(3, 'three'), + _t(4, 'four'), + _t(5, 'five'), + _t(6, 'six'), + _t(7, 'seven'), + _t(8, 'eight'), + _t(9, 'nine'), + _t(10, 'ten'), + _t(11, 'eleven'), + _t(12, 'twelve'), + _t(13, 'thirteen'), + _t(14, 'fourteen'), + _t(15, 'fifteen'), + _t(16, 'sixteen'), + _t(17, 'seventeen'), + _t(18, 'eighteen'), + _t(19, 'nineteen'), + _t(20, 'twenty'), + _t(21, 'twenty-one'), + _t(23, 'twenty-three'), + _t(25, 'twenty-five'), + _t(27, 'twenty-seven'), + _t(29, 'twenty-nine'), + _t(30, 'thirty'), + _t(32, 'thirty-two'), + _t(34, 'thirty-four'), + _t(36, 'thirty-six'), + _t(38, 'thirty-eight'), + _t(40, 'forty'), + _t(41, 'forty-one'), + _t(42, 'forty-two'), + _t(50, 'fifty'), + _t(53, 'fifty-three'), + _t(54, 'fifty-four'), + _t(60, 'sixty'), + _t(65, 'sixty-five'), + _t(66, 'sixty-six'), + _t(70, 'seventy'), + _t(77, 'seventy-seven'), + _t(78, 'seventy-eight'), + _t(80, 'eighty'), + _t(89, 'eighty-nine'), + _t(90, 'ninety'), + _t(91, 'ninety-one'), + _t(97, 'ninety-seven'), + _t(100, 'one hundred'), + _t(200, 'two hundred'), + _t(500, 'five hundred'), + _t(900, 'nine hundred'), + _t(101, 'one hundred one'), + _t(304, 'three hundred four'), + _t(410, 'four hundred ten'), + _t(617, 'six hundred seventeen'), + _t(712, 'seven hundred twelve'), + _t(835, 'eight hundred thirty-five'), + ]; + + testData.forEach(({ input, expected }) => { + it(`returns "${expected}" when given ${input}`, () => { + expect(numberToWords(input)).toEqual(expected); + }); + }); +}); diff --git a/src/lib/numberToWords.ts b/src/lib/numberToWords.ts new file mode 100644 index 0000000..26f7aae --- /dev/null +++ b/src/lib/numberToWords.ts @@ -0,0 +1,85 @@ +const numbersMapping: Map = new Map([ + [0, 'zero'], + [1, 'one'], + [2, 'two'], + [3, 'three'], + [4, 'four'], + [5, 'five'], + [6, 'six'], + [7, 'seven'], + [8, 'eight'], + [9, 'nine'], + [10, 'ten'], + [11, 'eleven'], + [12, 'twelve'], + [13, 'thirteen'], + [14, 'fourteen'], + [15, 'fifteen'], + [16, 'sixteen'], + [17, 'seventeen'], + [18, 'eighteen'], + [19, 'nineteen'], + [20, 'twenty'], + [30, 'thirty'], + [40, 'forty'], + [50, 'fifty'], + [60, 'sixty'], + [70, 'seventy'], + [80, 'eighty'], + [90, 'ninety'], +]); + +const { floor } = Math; + +function isInteger(n: number): boolean { + return floor(n) === n; +} + +function hundredsDigit(n: number): number { + return floor((n / 100) % 100); +} + +function tensValue(n: number): number { + return floor((n / 10) % 10) * 10; +} + +function onesValue(n: number): number { + return n % 10; +} + +// eslint-disable-next-line complexity +function numberToWords(number: number): string { + const mapped = numbersMapping.get(number); + if (typeof mapped === 'string') { + return mapped; + } + + let numberStr = ''; + if (isInteger(number)) { + const hundredth = hundredsDigit(number); + const hundreds = numbersMapping.get(hundredth); + if (hundredth !== 0 && typeof hundreds === 'string') { + numberStr += ` ${hundreds} hundred`; + } + + const tenth = tensValue(number); + const oneth = onesValue(number); + + if (tenth === 10 && oneth > 0) { + numberStr += ` ${numbersMapping.get(tenth + oneth) ?? ''}`; + } else { + const tens = numbersMapping.get(tenth); + if (tenth !== 0 && typeof tens === 'string') { + numberStr += ` ${tens}`; + } + + const ones = numbersMapping.get(oneth); + if (oneth !== 0 && typeof ones === 'string') { + numberStr += `${tenth !== 0 ? '-' : ' '}${ones}`; + } + } + } + return numberStr.trim(); +} + +export default numberToWords; diff --git a/src/pages/additionFillTheBlanks/AdditionSentence.tsx b/src/pages/additionFillTheBlanks/AdditionSentence.tsx index cb60807..e710341 100644 --- a/src/pages/additionFillTheBlanks/AdditionSentence.tsx +++ b/src/pages/additionFillTheBlanks/AdditionSentence.tsx @@ -1,5 +1,6 @@ import React from 'react'; import Blank from '../../components/Blank'; +import ProblemListItem from '../../components/ProblemListItem'; import { randomGenerator } from '../../lib/RandomNumberGenerator'; import roundRobinRange from '../../lib/roundRobinRange'; import Addition from './Addition'; @@ -63,10 +64,10 @@ function AdditionSentence({ const BlankOrNumber = blankOrNumberGenerator(blank, showAnswer); const label = `Addition Problem${showAnswer ? ' Answer' : ''}`; return ( -
  • -
  • + ); } diff --git a/src/pages/additionFillTheBlanks/AftbData.ts b/src/pages/additionFillTheBlanks/AftbData.ts index 13c8a7f..05ded80 100644 --- a/src/pages/additionFillTheBlanks/AftbData.ts +++ b/src/pages/additionFillTheBlanks/AftbData.ts @@ -1,3 +1,4 @@ +import Range from '../../lib/Range'; import FontSizeData from '../../components/forms/FontSizeData'; export type BlankPositionStrategy = 'sum' | 'addends' | 'random'; @@ -8,10 +9,6 @@ export const problemGenerations: Map = new Map([ ['custom addends', 'Custom Addends'], ]); -interface Range { - from: number; - to: number; -} export default interface AftbData extends FontSizeData { rangeFrom: number; rangeTo: number; diff --git a/src/pages/additionFillTheBlanks/PreviewAftb.tsx b/src/pages/additionFillTheBlanks/PreviewAftb.tsx index 4cb7656..bbe82f9 100644 --- a/src/pages/additionFillTheBlanks/PreviewAftb.tsx +++ b/src/pages/additionFillTheBlanks/PreviewAftb.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { makeStyles, Typography } from '@material-ui/core'; -import MultiPaperPage, { Builder, Props } from '../../components/MultiPaperPage'; +import MultiPaperPage, { Builder } from '../../components/MultiPaperPage'; import AdditionSentence, { AdditionBlankPosition, blankTypes, blankTypesAddends, generateAdditionSentences, } from './AdditionSentence'; @@ -9,40 +8,13 @@ import WorksheetHeader from '../../components/WorksheetHeader'; import WorksheetFooter from '../../components/WorksheetFooter'; import Addition from './Addition'; import { randomGenerator } from '../../lib/RandomNumberGenerator'; +import PageTitle from '../../elements/PageTitle'; +import ProblemList from '../../components/ProblemList'; interface PreviewAftbProps { aftbData: AftbData; } -const pageStyles = makeStyles(() => ({ - heading: { - textAlign: 'center', - }, - // All em units equivalent are based on a 20px font size base - list: { - margin: '5mm 0 0 0', - padding: 0, - // fontSize: '20px', - columnCount: 2, - columnWidth: 'auto', - counterReset: 'problem 0', - - '& > li': { - padding: '1.15em 0 1.15em 1.15em', // '6mm 0 6mm 6mm', // 23px - marginLeft: '1.9em', // '10mm', // 38px - counterIncrement: 'problem', - '-webkit-column-break-inside': 'avoid', - pabeBreakInside: 'avoid', - breakInside: 'avoid', - }, - - '& > li::marker': { - content: 'counter(problem) "."', - fontSize: '0.8em', // 16px - }, - }, -})); - function blankTypeFromStrategy(blankStrategy: BlankPositionStrategy): AdditionBlankPosition { switch (blankStrategy) { case 'addends': @@ -67,7 +39,6 @@ interface AdditionAndMeta { function PreviewAftb({ aftbData, }: PreviewAftbProps): JSX.Element { - const classes = pageStyles(); const data = generateAdditionSentences(aftbData).map((addition): AdditionAndMeta => { const { blankStrategy } = aftbData; const blank = blankTypeFromStrategy(blankStrategy); @@ -98,49 +69,19 @@ function PreviewAftb({ )} footer={()} - wrapper="ol" - wrapperProps={{ - className: `${classes.list} problems`, - 'aria-label': 'Problems', - }} + wrapper={ProblemList} + wrapperProps={{ columns: aftbData.columns }} data-test-id="problems" - wrapperPropsCallback={ - (props, { memberIndex }) => ({ - ...props, - style: { - counterReset: `problem ${memberIndex}`, - columns: aftbData.columns, - }, - } as Props) - } data={data} itemSelector=".addition-sentence-item" renderItems={problemBuilder} /> header={( - - Answer Key - + Answer Key )} wrapper="ol" - wrapperProps={{ - className: `${classes.list} answers`, - 'aria-label': 'Answers', - }} - wrapperPropsCallback={ - (props, { memberIndex }) => ({ - ...props, - style: { - counterReset: `problem ${memberIndex}`, - columns: aftbData.columns, - }, - } as Props) - } + wrapperProps={{ className: 'answers', label: 'Answers' }} data={data} itemSelector=".addition-sentence-item" renderItems={ diff --git a/src/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx index ddc6dd4..c2f57fb 100644 --- a/src/pages/main/MainPage.tsx +++ b/src/pages/main/MainPage.tsx @@ -47,6 +47,10 @@ const worksheetlinks: NavigationLink[] = [ path: '/worksheet-place-values', text: 'Place Values', }, + { + path: '/worksheet-numbers-to-words', + text: 'Numbers to Words', + }, ]; function MainPage(): JSX.Element { diff --git a/src/pages/numbersToWords/CustomizeNumbersToWordsForm.tsx b/src/pages/numbersToWords/CustomizeNumbersToWordsForm.tsx new file mode 100644 index 0000000..6d3e595 --- /dev/null +++ b/src/pages/numbersToWords/CustomizeNumbersToWordsForm.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import CustomizeForm from '../../components/forms/CustomizeForm'; +import NumberField from '../../components/forms/NumberField'; +import NumberRangeSlider from '../../components/forms/NumberRangeSlider'; +import numberOrEmpty from '../../lib/numberOrEmpty'; +import NumbersToWordsData from './NumbersToWordsData'; + +interface CustomizeNumbersToWordsFormProps { + onChange: (data: NumbersToWordsData) => void; + data: NumbersToWordsData; +} + +function CustomizeNumbersToWordsForm({ + data, onChange, +}: CustomizeNumbersToWordsFormProps): JSX.Element { + return ( + + onChange({ ...data, count })} + /> + { + onChange({ ...data, range }); + }} + from={data.range.from} + to={data.range.to} + magnitude={3} + /> + + ); +} + +export default CustomizeNumbersToWordsForm; diff --git a/src/pages/numbersToWords/NumbersToWordsData.tsx b/src/pages/numbersToWords/NumbersToWordsData.tsx new file mode 100644 index 0000000..1579085 --- /dev/null +++ b/src/pages/numbersToWords/NumbersToWordsData.tsx @@ -0,0 +1,8 @@ +import Range from '../../lib/Range'; + +interface NumbersToWordsData { + range: Range; + count: number; +} + +export default NumbersToWordsData; diff --git a/src/pages/numbersToWords/NumbersToWordsPage.tsx b/src/pages/numbersToWords/NumbersToWordsPage.tsx new file mode 100644 index 0000000..08892c7 --- /dev/null +++ b/src/pages/numbersToWords/NumbersToWordsPage.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import PrintableUI from '../../components/PrintableUI'; +import usePageState from '../usePageState'; +import CustomizeNumbersToWordsForm from './CustomizeNumbersToWordsForm'; +import NumbersToWordsData from './NumbersToWordsData'; +import PreviewNumbersToWords from './PreviewNumbersToWords'; + +const defaultData: NumbersToWordsData = { + range: { from: 0, to: 9 }, + count: 10, +}; +const key = 'numbersToWords'; + +function NumbersToWordsPage(): JSX.Element { + const { data, onChange } = usePageState({ + key, defaultData, + }); + return ( + + )} + > + + + ); +} + +export default NumbersToWordsPage; diff --git a/src/pages/numbersToWords/PreviewNumbersToWords.tsx b/src/pages/numbersToWords/PreviewNumbersToWords.tsx new file mode 100644 index 0000000..d686923 --- /dev/null +++ b/src/pages/numbersToWords/PreviewNumbersToWords.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import Blank from '../../components/Blank'; +import MultiPaperPage from '../../components/MultiPaperPage'; +import ProblemList from '../../components/ProblemList'; +import ProblemListItem from '../../components/ProblemListItem'; +import WorksheetFooter from '../../components/WorksheetFooter'; +import WorksheetHeader from '../../components/WorksheetHeader'; +import PageTitle from '../../elements/PageTitle'; +import numberToWords from '../../lib/numberToWords'; +import { randomGenerator } from '../../lib/RandomNumberGenerator'; +import NumbersToWordsData from './NumbersToWordsData'; + +function generateProblems({ count, range }: NumbersToWordsData): Array { + const min = range.from; + const max = range.to; + const problems: Array = []; + const track: Set = new Set([]); + const limit = max - min; + while (problems.length < count) { + const number = randomGenerator.integer(max, min); + if (!track.has(number)) { + problems.push(number); + track.add(number); + } + if (problems.length % limit === 0) { + track.clear(); + } + } + return problems; +} + +interface PreviewNumbersToWordsProps { + customData: NumbersToWordsData; +} + +function PreviewNumbersToWords({ customData }: PreviewNumbersToWordsProps): JSX.Element { + const problems = generateProblems(customData); + + const itemBuilder = (showAnswer: boolean) => { + function fn(number: number, indexNumber: number) { + return ( + + { + numberToWords(number) + } + {': '} + + + ); + } + return fn; + }; + return ( + <> + +

    Write the number that is written in words.

    + + )} + footer={()} + wrapper={ProblemList} + data={problems} + itemSelector=".numbers-to-words-problem-item" + renderItems={itemBuilder(false)} + /> + Answer Key)} + wrapper={ProblemList} + wrapperProps={{ label: 'Answers' }} + data={problems} + itemSelector=".numbers-to-words-problem-item" + renderItems={itemBuilder(true)} + /> + + ); +} + +export default PreviewNumbersToWords; diff --git a/src/pages/patterns/PreviewPatterns.tsx b/src/pages/patterns/PreviewPatterns.tsx index 234d2b6..68667bc 100644 --- a/src/pages/patterns/PreviewPatterns.tsx +++ b/src/pages/patterns/PreviewPatterns.tsx @@ -3,6 +3,8 @@ import { makeStyles } from '@material-ui/core/node_modules/@material-ui/styles'; import React from 'react'; import FontLoad from '../../components/FontLoad'; import MultiPaperPage, { Builder } from '../../components/MultiPaperPage'; +import ProblemList from '../../components/ProblemList'; +import ProblemListItem from '../../components/ProblemListItem'; import WorksheetFooter from '../../components/WorksheetFooter'; import WorksheetHeader from '../../components/WorksheetHeader'; import PatternGenerator from '../../lib/PatternGenerator'; @@ -30,13 +32,15 @@ interface PatternProblem { } const problemStyles = makeStyles(() => ({ + listItem: { + paddingTop: '0.15em', + paddingBottom: '0.15em', + }, pattern: { fontSize: 48, fontFamily: "'Sawarabi Gothic', sans-serif", display: 'inline-block', verticalAlign: 'middle', - padding: '0 0 0 2mm', - margin: '0 0 2mm', }, patternElement: { padding: '0 2mm', @@ -58,25 +62,25 @@ const problemStyles = makeStyles(() => ({ function PatternProblemDisplay({ elements }: PatternProblem): JSX.Element { const classes = problemStyles(); return ( -
  • +
    { elements.map((shape, index): JSX.Element => ( { - index === (elements.length - 1) - ? ( - - __ - - ) - : ({ shape }) - } + index === (elements.length - 1) + ? ( + + __ + + ) + : ({shape}) + } )) }
    -
  • + ); } @@ -109,8 +113,7 @@ function PreviewPatterns({ patternsData }: PreviewPatternsProps): JSX.Element { )} footer={()} - wrapper="ol" - wrapperProps={{ className: 'problems' }} + wrapper={ProblemList} data={data} itemSelector=".pattern-problem-item" renderItems={itemBuilder} diff --git a/src/pages/placeValues/PreviewPlaceValues.tsx b/src/pages/placeValues/PreviewPlaceValues.tsx index 4df080a..50bc3b4 100644 --- a/src/pages/placeValues/PreviewPlaceValues.tsx +++ b/src/pages/placeValues/PreviewPlaceValues.tsx @@ -1,10 +1,12 @@ -import { Typography } from '@material-ui/core'; import makeStyles from '@material-ui/core/styles/makeStyles'; import React from 'react'; import Blank, { BlankProps } from '../../components/Blank'; -import MultiPaperPage, { Props } from '../../components/MultiPaperPage'; +import MultiPaperPage from '../../components/MultiPaperPage'; +import ProblemList from '../../components/ProblemList'; +import ProblemListItem from '../../components/ProblemListItem'; import WorksheetFooter from '../../components/WorksheetFooter'; import WorksheetHeader from '../../components/WorksheetHeader'; +import PageTitle from '../../elements/PageTitle'; import { randomGenerator } from '../../lib/RandomNumberGenerator'; import PlaceValuesData from './PlaceValuesData'; import PlaceValuesProblem from './PlaceValuesProblem'; @@ -28,23 +30,15 @@ function generateProblems({ count, magnitude }: PlaceValuesData): Array ({ - heading: { - textAlign: 'center', - }, list: { - '& > li': { - fontSize: 20, - padding: '1.15em 0 1.15em 1.15em', // '6mm 0 6mm 6mm', // 23px - counterIncrement: 'problem', - '-webkit-column-break-inside': 'avoid', - pabeBreakInside: 'avoid', - breakInside: 'avoid', - }, '& .and': { padding: '0 1.15em', display: 'inline-block', @@ -88,10 +82,10 @@ function PreviewPlaceValues({ customData }: PreviewPlaceValuesProps): JSX.Elemen const itemBuilder = (showAnswer: boolean) => { function fn(problem: PlaceValuesProblem, indexNumber: number) { return ( -
  • { customData.magnitude === 'hundreds' @@ -103,7 +97,7 @@ function PreviewPlaceValues({ customData }: PreviewPlaceValuesProps): JSX.Elemen = {' '} {problem.number} -
  • + ); } return fn; @@ -116,7 +110,7 @@ function PreviewPlaceValues({ customData }: PreviewPlaceValuesProps): JSX.Elemen

    Fill out the correct number for each place value.

    )} - wrapper="ol" + wrapper={ProblemList} footer={()} wrapperProps={{ className: `problems bar ${classes.list} foo` }} data={problems} @@ -125,28 +119,13 @@ function PreviewPlaceValues({ customData }: PreviewPlaceValuesProps): JSX.Elemen /> - Answer Key -
    + Answer Key )} - wrapper="ol" - footer={()} + wrapper={ProblemList} wrapperProps={{ className: `problems bar ${classes.list}`, - 'aria-label': 'Answers', + label: 'Answers', }} - wrapperPropsCallback={ - (props, { memberIndex }) => ({ - ...props, - style: { - counterReset: `problem ${memberIndex}`, - }, - } as Props) - } data={problems} itemSelector=".place-value-problem-item" renderItems={itemBuilder(true)}