diff --git a/cypress/e2e/navigation.cy.ts b/cypress/e2e/navigation.cy.ts index cf05579..39291e7 100644 --- a/cypress/e2e/navigation.cy.ts +++ b/cypress/e2e/navigation.cy.ts @@ -48,27 +48,28 @@ it('can visit all subpages', () => { clickWorksheetLink(/subtraction.+fill.+blank/i); cy.hasCustomizeFormHeading(/subtraction.+fill.+blank/i); - goBackHome(); - // Pattern Worksheets + goBackHome(); clickWorksheetLink(/patterns/i); cy.hasCustomizeFormHeading(/patterns/i); - goBackHome(); - // Place Value Worksheets + goBackHome(); clickWorksheetLink(/place values/i); cy.hasCustomizeFormHeading(/place values/i); - goBackHome(); - // Numbers to Words + goBackHome(); clickWorksheetLink(/numbers to words/i); cy.hasCustomizeFormHeading(/numbers to words/i); + // Numbers to Words goBackHome(); + clickWorksheetLink(/telling time/i); + cy.hasCustomizeFormHeading(/telling time/i); // Settings Page + goBackHome(); cy.findByRole('button', { name: /open menu/i }).click(); cy.findByRole('navigation', { name: /sidebar/i }) .find('a:contains("Settings")') diff --git a/cypress/e2e/worksheet_telling_time.cy.ts b/cypress/e2e/worksheet_telling_time.cy.ts new file mode 100644 index 0000000..0ea643c --- /dev/null +++ b/cypress/e2e/worksheet_telling_time.cy.ts @@ -0,0 +1,10 @@ +it('can create telling time worksheet', () => { + cy.visitWorksheetTellingTime(); + + cy.findByLabelText(/count/i).clearType(7); + + cy.withinPreview(() => { + cy.problemListItems() + .should('have.length', 7); + }); +}); diff --git a/cypress/index.d.ts b/cypress/index.d.ts index 47d2195..891f83a 100644 --- a/cypress/index.d.ts +++ b/cypress/index.d.ts @@ -13,6 +13,7 @@ declare namespace Cypress { visitWorksheetVerticalAddition(): Chainable; visitWorksheetSubtractionWithFigures(): Chainable; visitWorksheetSubtractionFillInTheBlanks(): Chainable; + visitWorksheetTellingTime(): Chainable; } interface Chainable { @@ -25,7 +26,7 @@ declare namespace Cypress { reactComponent(fn?: (prevSubject: Subject) => void): Chainable; setNumberRange(label: string | RegExp, min: number, max: number): Chainable; findPaperPage(page: number): Chainable; - clearType(value: string): Chainable; + clearType(value: string | number): Chainable; hasCustomizeFormHeading(text: LabelPattern): Chainable; problemListItems(): Chainable; answerListItems(): Chainable; diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 5faf32f..d5b4152 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -38,6 +38,7 @@ const paths = { worksheetVerticalAddition: '/worksheet-vertical-addition', worksheetSubtractionWithFigures: '/worksheet-subtraction-with-figures', worksheetSubtractionFillInTheBlanks: '/worksheet-subtraction-fill-in-the-blanks', + worksheetTellingTime: '/worksheet-telling-time', }; const pathNames = Object.keys(paths); diff --git a/src/components/forms/CustomizeForm.tsx b/src/components/forms/CustomizeForm.tsx index 5a0fdef..4dbc4d4 100644 --- a/src/components/forms/CustomizeForm.tsx +++ b/src/components/forms/CustomizeForm.tsx @@ -14,6 +14,7 @@ import SectionPageTitle from '../../elements/SectionPageTitle'; import PaperSize, { Orientation } from '../../lib/PaperSize'; import PrintHelpDialog from './PrintHelpDialog'; import SelectFooterField from './SelectFooterField'; +import zeroPad from '../../lib/zeroPad'; interface CustomizeFormProps { onBeforePrint?: () => boolean; @@ -26,10 +27,6 @@ interface EventWithSubmitter extends Event { submitter: HTMLButtonElement | undefined; } -function zeroPad(n: number): string { - return n > 9 ? n.toString() : `0${n}`; -} - function timeStamp(): string { const now = new Date(); return `${now.getFullYear()}-${zeroPad(now.getMonth())}-${zeroPad(now.getDate())}`; diff --git a/src/lib/RandomNumberGenerator.ts b/src/lib/RandomNumberGenerator.ts index 3944a12..af2ee3f 100644 --- a/src/lib/RandomNumberGenerator.ts +++ b/src/lib/RandomNumberGenerator.ts @@ -1,5 +1,6 @@ import NumberGenerator from './NumberGenerator'; import { maxMustNotBeLessThanMin } from './numberGeneratorErrors'; +import Range from './Range'; export type MathRandom = () => number; @@ -57,6 +58,10 @@ class RandomNumberGenerator implements NumberGenerator { return asInteger(this.rand(), max, min); } + integerR(range: Range): number { + return this.integer(range.to, range.from); + } + integerBiasLess(max: number, min = 0): number { return asInteger(circular(this.rand()), max, min); } diff --git a/src/lib/linkMap.ts b/src/lib/linkMap.ts index 22da802..83c54bc 100644 --- a/src/lib/linkMap.ts +++ b/src/lib/linkMap.ts @@ -11,6 +11,7 @@ const SubtractionFillInTheBlanksPage = lazy(() => import('../pages/subtractionFi const PatternsPage = lazy(() => import('../pages/patterns/PatternsPage')); const PlaceValuesPage = lazy(() => import('../pages/placeValues/PlaceValuesPage')); const NumbersToWordsPage = lazy(() => import('../pages/numbersToWords/NumbersToWordsPage')); +const TellingTimePage = lazy(() => import('../pages/tellingTime/TellingTimePage')); const ExperimentsPage = lazy(() => import('../pages/experiments/ExperimentsPage')); const SettingsPage = lazy(() => import('../pages/settings/SettingsPage')); @@ -72,6 +73,10 @@ export const mathLinks: SectionLinks = new Map([ text: 'Numbers to Words', loader: NumbersToWordsPage, }], + ['/worksheet-telling-time', { + text: 'Telling Time', + loader: TellingTimePage, + }], ]); export const miscLinks: SectionLinks = new Map([ diff --git a/src/lib/zeroPad.ts b/src/lib/zeroPad.ts new file mode 100644 index 0000000..5ca185e --- /dev/null +++ b/src/lib/zeroPad.ts @@ -0,0 +1,3 @@ +export default function zeroPad(n: number): string { + return n < 10 ? `0${n}` : n.toString(); +} diff --git a/src/pages/tellingTime/ClockFace.tsx b/src/pages/tellingTime/ClockFace.tsx new file mode 100644 index 0000000..f2b6cdb --- /dev/null +++ b/src/pages/tellingTime/ClockFace.tsx @@ -0,0 +1,290 @@ +/* eslint-disable max-len */ +import React from 'react'; + +interface ClockFaceProps { + hour: number; + minute: number; + width?: number; + seconds?: boolean; +} + +const DEFAULT_WIDTH = 120; + +function ClockFace({ + hour, minute, width = DEFAULT_WIDTH, seconds = false, +}: ClockFaceProps): JSX.Element { + const hourRotation = 360 * ((hour + (minute / 60)) / 12); + const minuteRotation = 360 * (minute / 60); + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + + 7 + + + 8 + + + 9 + + + 10 + + + 11 + + + 12 + + + + + + { + seconds + ? ( + + ) + : null + } + + + + +
+ ); +} + +ClockFace.defaultProps = { + width: DEFAULT_WIDTH, + seconds: false, +}; + +export default ClockFace; diff --git a/src/pages/tellingTime/CustomizeTellingTimeForm.tsx b/src/pages/tellingTime/CustomizeTellingTimeForm.tsx new file mode 100644 index 0000000..9b4ee3e --- /dev/null +++ b/src/pages/tellingTime/CustomizeTellingTimeForm.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Divider } from '@material-ui/core'; +import CustomizeForm from '../../components/forms/CustomizeForm'; +import TellingTimeData, { ProblemType, problemTypeOptions } from './TellingTimeData'; +import NumberField from '../../components/forms/NumberField'; +import numberOrEmpty from '../../lib/numberOrEmpty'; +import SelectField from '../../components/forms/SelectField'; +import stringMapToOptions from '../../components/forms/stringMapToOptions'; + +interface CustomizeTellingTimeFormProps { + onChange: (data: TellingTimeData) => void; + data: TellingTimeData; +} + +function CustomizeTellingTimeForm({ + data, onChange, +}: CustomizeTellingTimeFormProps): JSX.Element { + return ( + + onChange({ ...data, count })} + /> + { + onChange({ + ...data, + problemType: value as ProblemType, + }); + }} + > + {stringMapToOptions(problemTypeOptions)} + + + onChange({ ...data, columns })} + /> + + ); +} + +export default CustomizeTellingTimeForm; diff --git a/src/pages/tellingTime/PreviewTellingTime.tsx b/src/pages/tellingTime/PreviewTellingTime.tsx new file mode 100644 index 0000000..05fac72 --- /dev/null +++ b/src/pages/tellingTime/PreviewTellingTime.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { makeStyles } from '@material-ui/core'; +import MultiPaperPage from '../../components/MultiPaperPage'; +import PageTitle from '../../elements/PageTitle'; +import ProblemList from '../../components/ProblemList'; +import ProblemListItem from '../../components/ProblemListItem'; +import WorksheetFooter from '../../components/printElements/WorksheetFooter'; +import WorksheetHeader from '../../components/printElements/WorksheetHeader'; +import TellingTimeData from './TellingTimeData'; +import SimpleTime from './SimpleTime'; +import { randomGenerator } from '../../lib/RandomNumberGenerator'; +import Range from '../../lib/Range'; +import randomElement from '../../lib/randomElement'; +import ClockFace from './ClockFace'; +import Blank from '../../components/Blank'; + +const hourRange: Range = { from: 1, to: 12 }; +const minuterRange: Range = { from: 0, to: 59 }; + +function generateProblems({ problemType, count }: TellingTimeData): SimpleTime[] { + const problems: SimpleTime[] = []; + let minuteGen: () => number; + switch (problemType) { + case 'hours': + minuteGen = () => 0; + break; + + case 'hours and half hours': + minuteGen = () => randomElement([0, 30]); + break; + + case '5-minute intervals': + minuteGen = () => randomGenerator.integerR({ from: 0, to: 11 }) * 5; + break; + + default: + minuteGen = () => randomGenerator.integerR(minuterRange); + break; + } + for (let i = 0; i < count; i++) { + const hour = randomGenerator.integerR(hourRange); + problems.push( + new SimpleTime(hour, minuteGen()), + ); + } + + return problems; +} + +const itemStyles = makeStyles(() => ({ + wrapper: { + display: 'inline-block', + verticalAlign: 'text-top', + textAlign: 'center', + paddingBottom: '4mm', + }, +})); + +function itemBuilder( + showAnswer: boolean, +) { + const classes = itemStyles(); + function fn(time: SimpleTime, indexNumber: number) { + return ( + +
+ + +
+
+ ); + } + return fn; +} + +interface PreviewTellingTimeProps { + data: TellingTimeData; +} + +function PreviewTellingTime({ data }: PreviewTellingTimeProps): JSX.Element { + const problems = generateProblems(data); + const instructions = 'Write the time shown on the clock.'; + + return ( + <> + +

{instructions}

+ + )} + wrapper={ProblemList} + footer={()} + wrapperProps={{ + className: 'problems', + columns: data.columns, + }} + data={problems} + itemSelector=".telling-time-problem-item" + renderItems={itemBuilder(false)} + /> + Answer Key + )} + wrapper={ProblemList} + wrapperProps={{ + className: 'answers', + label: 'Answers', + columns: data.columns, + }} + data={problems} + itemSelector=".telling-time-problem-item" + renderItems={itemBuilder(true)} + /> + + ); +} + +export default PreviewTellingTime; diff --git a/src/pages/tellingTime/SimpleTime.spec.ts b/src/pages/tellingTime/SimpleTime.spec.ts new file mode 100644 index 0000000..8f0afa5 --- /dev/null +++ b/src/pages/tellingTime/SimpleTime.spec.ts @@ -0,0 +1,37 @@ +import SimpleTime from './SimpleTime'; + +interface Example { + input: number[], + expected: string, +} + +describe('SimpleTime', () => { + describe('toString()', () => { + const examples: Example[] = [ + { + input: [6, 30], + expected: '6:30', + }, + { + input: [1, 0], + expected: '1:00', + }, + { + input: [12, 0], + expected: '12:00', + }, + { + input: [4, 7], + expected: '4:07', + }, + ]; + + for (const { input, expected } of examples) { + it(`returns correct string for ${expected}`, () => { + const [hour, minute] = input; + const time = new SimpleTime(hour, minute); + expect(time.toString()).toEqual(expected); + }); + } + }); +}); diff --git a/src/pages/tellingTime/SimpleTime.ts b/src/pages/tellingTime/SimpleTime.ts new file mode 100644 index 0000000..00a7f3b --- /dev/null +++ b/src/pages/tellingTime/SimpleTime.ts @@ -0,0 +1,18 @@ +import zeroPad from '../../lib/zeroPad'; + +class SimpleTime { + hour: number; + + minute: number; + + constructor(hour: number, minute: number) { + this.hour = hour; + this.minute = minute; + } + + toString(): string { + return `${this.hour}:${zeroPad(this.minute)}`; + } +} + +export default SimpleTime; diff --git a/src/pages/tellingTime/TellingTimeData.ts b/src/pages/tellingTime/TellingTimeData.ts new file mode 100644 index 0000000..299b1a0 --- /dev/null +++ b/src/pages/tellingTime/TellingTimeData.ts @@ -0,0 +1,12 @@ +export type ProblemType = 'hours' | 'hours and half hours' | '5-minute intervals'; +export const problemTypeOptions = new Map([ + ['hours', 'Hours'], + ['hours and half hours', 'Hours and Half Hours'], + ['5-minute intervals', '5-minute Intervals'], +]); + +export default interface TellingTimeData { + count: number; + columns: number; + problemType: ProblemType; +} diff --git a/src/pages/tellingTime/TellingTimePage.tsx b/src/pages/tellingTime/TellingTimePage.tsx new file mode 100644 index 0000000..faadb13 --- /dev/null +++ b/src/pages/tellingTime/TellingTimePage.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PrintableUI from '../../components/PrintableUI'; +import usePageState from '../usePageState'; +import CustomizeTellingTimeForm from './CustomizeTellingTimeForm'; +import TellingTimeData from './TellingTimeData'; +import PreviewTellingTime from './PreviewTellingTime'; + +const defaultData: TellingTimeData = { + count: 9, + problemType: 'hours', + columns: 3, +}; +const key = 'tellingTime'; + +function TellingTimePage(): JSX.Element { + const { data, onChange } = usePageState({ + key, defaultData, + }); + return ( + + )} + > + + + ); +} + +export default TellingTimePage; diff --git a/src/pages/tellingTime/temp.tsx b/src/pages/tellingTime/temp.tsx new file mode 100644 index 0000000..43935e6 --- /dev/null +++ b/src/pages/tellingTime/temp.tsx @@ -0,0 +1,227 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; + +function SvgComponent(props: SVGProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + + 7 + + + 8 + + + 9 + + + 10 + + + 11 + + + 12 + + + + + + + + + ); +} + +export default SvgComponent;