From f6a30537cdf972ea57c29ae7782ef2fb4125bcd4 Mon Sep 17 00:00:00 2001 From: Matt George Date: Mon, 18 Dec 2023 22:26:18 +0000 Subject: [PATCH 01/21] Store the timers in localstorage --- src/App.test.tsx | 2 +- src/App.tsx | 114 ++++++++++++++++++++++++++--------------------- src/index.tsx | 2 +- 3 files changed, 64 insertions(+), 54 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index de5e198..d8a746e 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, waitFor } from "@testing-library/react"; -import App from "./App"; +import { App } from "./App"; describe("", () => { test("renders the basic form", () => { diff --git a/src/App.tsx b/src/App.tsx index c849fd8..3648fae 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,19 @@ +import { ArrayHelpers, FieldArray, Form, Formik } from "formik"; import React, { useCallback, useEffect, useState } from "react"; -import { Formik, Form, FieldArray, ArrayHelpers } from "formik"; -import { FoodItemField } from "./FoodItemField"; +import { Contact } from "./Contact"; import { CookingTimeField } from "./CookingTimeField"; +import { FoodItemField } from "./FoodItemField"; import { Heading } from "./Heading"; import { Instructions } from "./Instructions"; -import { Contact } from "./Contact"; -interface Timer { +type Timer = { timeName: string; timeLength: number | undefined; timeAfter: number; -} -interface Values { +}; +type Values = { timers: Timer[]; -} +}; function handleUndefinedTimeLength(number: number | undefined) { if (number) { @@ -26,44 +26,44 @@ function minuteWording(number: number) { return "minute".concat(number > 1 ? "s" : ""); } -function generateHtmlInstructions(baseTimers: Timer[] | undefined) { - if (!baseTimers || !baseTimers[0]) { +function generateHtmlInstructions(formTimers: Timer[] | undefined) { + if (!formTimers || !formTimers[0]) { return undefined; } const elements = [ - {baseTimers[0].timeName} goes in first for{" "} + {formTimers[0].timeName} goes in first for{" "} - {baseTimers[0].timeLength}{" "} + {formTimers[0].timeLength}{" "} {minuteWording( - handleUndefinedTimeLength(baseTimers[0].timeLength) + handleUndefinedTimeLength(formTimers[0].timeLength) )} , ]; - for (let i = 1; i < baseTimers.length; i++) { - if (baseTimers[i].timeAfter === 0) { + for (let i = 1; i < formTimers.length; i++) { + if (formTimers[i].timeAfter === 0) { elements.push( - {baseTimers[i].timeName} starts at the{" "} + {formTimers[i].timeName} starts at the{" "} same time as{" "} - {baseTimers[i - 1].timeName} + {formTimers[i - 1].timeName} ); } else { elements.push( - {baseTimers[i].timeName} starts{" "} + {formTimers[i].timeName} starts{" "} - {baseTimers[i].timeAfter}{" "} - {minuteWording(baseTimers[i].timeAfter)} + {formTimers[i].timeAfter}{" "} + {minuteWording(formTimers[i].timeAfter)} {" "} - after {baseTimers[i - 1].timeName} and goes in for{" "} + after {formTimers[i - 1].timeName} and goes in for{" "} - {baseTimers[i].timeLength}{" "} + {formTimers[i].timeLength}{" "} {minuteWording( - handleUndefinedTimeLength(baseTimers[i].timeLength) + handleUndefinedTimeLength(formTimers[i].timeLength) )} @@ -72,33 +72,33 @@ function generateHtmlInstructions(baseTimers: Timer[] | undefined) { } return elements; } -function generatePlainTextInstructions(baseTimers: Timer[] | undefined) { - if (!baseTimers || !baseTimers[0]) { +function generatePlainTextInstructions(formTimers: Timer[] | undefined) { + if (!formTimers || !formTimers[0]) { return undefined; } const elements = [ - `1. ${baseTimers[0].timeName} goes in first for ${ - baseTimers[0].timeLength + `1. ${formTimers[0].timeName} goes in first for ${ + formTimers[0].timeLength } ${minuteWording( - handleUndefinedTimeLength(baseTimers[0].timeLength) + handleUndefinedTimeLength(formTimers[0].timeLength) )}`, ]; - for (let i = 1; i < baseTimers.length; i++) { - if (baseTimers[i].timeAfter === 0) { + for (let i = 1; i < formTimers.length; i++) { + if (formTimers[i].timeAfter === 0) { elements.push( `${i + 1}. ${ - baseTimers[i].timeName - } starts at the same time as ${baseTimers[i - 1].timeName}` + formTimers[i].timeName + } starts at the same time as ${formTimers[i - 1].timeName}` ); } else { elements.push( - `${i + 1}. ${baseTimers[i].timeName} starts ${ - baseTimers[i].timeAfter - } ${minuteWording(baseTimers[i].timeAfter)} after ${ - baseTimers[i - 1].timeName - } and goes in for ${baseTimers[i].timeLength} ${minuteWording( - handleUndefinedTimeLength(baseTimers[i].timeLength) + `${i + 1}. ${formTimers[i].timeName} starts ${ + formTimers[i].timeAfter + } ${minuteWording(formTimers[i].timeAfter)} after ${ + formTimers[i - 1].timeName + } and goes in for ${formTimers[i].timeLength} ${minuteWording( + handleUndefinedTimeLength(formTimers[i].timeLength) )}` ); } @@ -106,24 +106,34 @@ function generatePlainTextInstructions(baseTimers: Timer[] | undefined) { return elements; } -const App: React.FC = () => { +export function App() { + const [storedTimers, setStoredTimers] = useState( + window.localStorage.getItem("timers") + ); + + const [instructions, setInstructions] = useState(); + const [plainTextInstructions, setPlainTextInstructions] = + useState(); + + useEffect(() => { + const timers = window.localStorage.getItem("timers"); + if (timers) { + setPlainTextInstructions( + generatePlainTextInstructions(JSON.parse(timers)) + ); + setInstructions(generateHtmlInstructions(JSON.parse(timers))); + } + }, [storedTimers]); + const emptyTimer = { timeName: "", timeLength: undefined, timeAfter: 0, }; const initialValues: Values = { - timers: [emptyTimer], + timers: storedTimers ? JSON.parse(storedTimers) : [emptyTimer], }; - const [baseTimers, setBaseTimers] = useState(); - const [instructions, setInstructions] = useState(); - const [plainTextInstructions, setPlainTextInstructions] = - useState(); - - useEffect(() => { - setInstructions(generateHtmlInstructions(baseTimers)); - setPlainTextInstructions(generatePlainTextInstructions(baseTimers)); - }, [baseTimers]); + console.log({ storedTimers }); const handleSubmit = useCallback((values: Values) => { const formattedTimers = values.timers @@ -147,7 +157,9 @@ const App: React.FC = () => { ); } } - setBaseTimers(formattedTimers); + + setStoredTimers(JSON.stringify(formattedTimers)); + window.localStorage.setItem("timers", JSON.stringify(formattedTimers)); }, []); return ( @@ -234,6 +246,4 @@ const App: React.FC = () => { ); -}; - -export default App; +} diff --git a/src/index.tsx b/src/index.tsx index 3ff3c1f..8052930 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,7 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import { App } from "./App"; import "./index.css"; -import App from "./App"; const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement From e8d0736763eb2eea5b420cbdcf5575a9a8f96d15 Mon Sep 17 00:00:00 2001 From: Matt George Date: Mon, 18 Dec 2023 22:27:47 +0000 Subject: [PATCH 02/21] Move types to another file --- src/App.tsx | 12 ++---------- src/types.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 src/types.ts diff --git a/src/App.tsx b/src/App.tsx index 3648fae..94b4278 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,19 +1,11 @@ import { ArrayHelpers, FieldArray, Form, Formik } from "formik"; -import React, { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Contact } from "./Contact"; import { CookingTimeField } from "./CookingTimeField"; import { FoodItemField } from "./FoodItemField"; import { Heading } from "./Heading"; import { Instructions } from "./Instructions"; - -type Timer = { - timeName: string; - timeLength: number | undefined; - timeAfter: number; -}; -type Values = { - timers: Timer[]; -}; +import { Timer, Values } from "./types"; function handleUndefinedTimeLength(number: number | undefined) { if (number) { diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..5c87fc1 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,9 @@ +export type Timer = { + timeName: string; + timeLength: number | undefined; + timeAfter: number; +}; + +export type Values = { + timers: Timer[]; +}; From 0b6890316eec57ab98ccea84c5e197d49285bb8e Mon Sep 17 00:00:00 2001 From: Matt George Date: Mon, 18 Dec 2023 22:30:04 +0000 Subject: [PATCH 03/21] Move functions into utils file --- src/App.tsx | 98 ++++----------------------------------------------- src/utils.tsx | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 92 deletions(-) create mode 100644 src/utils.tsx diff --git a/src/App.tsx b/src/App.tsx index 94b4278..9517650 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,98 +5,12 @@ import { CookingTimeField } from "./CookingTimeField"; import { FoodItemField } from "./FoodItemField"; import { Heading } from "./Heading"; import { Instructions } from "./Instructions"; -import { Timer, Values } from "./types"; - -function handleUndefinedTimeLength(number: number | undefined) { - if (number) { - return number; - } - return 0; -} - -function minuteWording(number: number) { - return "minute".concat(number > 1 ? "s" : ""); -} - -function generateHtmlInstructions(formTimers: Timer[] | undefined) { - if (!formTimers || !formTimers[0]) { - return undefined; - } - - const elements = [ - - {formTimers[0].timeName} goes in first for{" "} - - {formTimers[0].timeLength}{" "} - {minuteWording( - handleUndefinedTimeLength(formTimers[0].timeLength) - )} - - , - ]; - for (let i = 1; i < formTimers.length; i++) { - if (formTimers[i].timeAfter === 0) { - elements.push( - - {formTimers[i].timeName} starts at the{" "} - same time as{" "} - {formTimers[i - 1].timeName} - - ); - } else { - elements.push( - - {formTimers[i].timeName} starts{" "} - - {formTimers[i].timeAfter}{" "} - {minuteWording(formTimers[i].timeAfter)} - {" "} - after {formTimers[i - 1].timeName} and goes in for{" "} - - {formTimers[i].timeLength}{" "} - {minuteWording( - handleUndefinedTimeLength(formTimers[i].timeLength) - )} - - - ); - } - } - return elements; -} -function generatePlainTextInstructions(formTimers: Timer[] | undefined) { - if (!formTimers || !formTimers[0]) { - return undefined; - } - - const elements = [ - `1. ${formTimers[0].timeName} goes in first for ${ - formTimers[0].timeLength - } ${minuteWording( - handleUndefinedTimeLength(formTimers[0].timeLength) - )}`, - ]; - for (let i = 1; i < formTimers.length; i++) { - if (formTimers[i].timeAfter === 0) { - elements.push( - `${i + 1}. ${ - formTimers[i].timeName - } starts at the same time as ${formTimers[i - 1].timeName}` - ); - } else { - elements.push( - `${i + 1}. ${formTimers[i].timeName} starts ${ - formTimers[i].timeAfter - } ${minuteWording(formTimers[i].timeAfter)} after ${ - formTimers[i - 1].timeName - } and goes in for ${formTimers[i].timeLength} ${minuteWording( - handleUndefinedTimeLength(formTimers[i].timeLength) - )}` - ); - } - } - return elements; -} +import { Values } from "./types"; +import { + generateHtmlInstructions, + generatePlainTextInstructions, + handleUndefinedTimeLength, +} from "./utils"; export function App() { const [storedTimers, setStoredTimers] = useState( diff --git a/src/utils.tsx b/src/utils.tsx new file mode 100644 index 0000000..1a1e889 --- /dev/null +++ b/src/utils.tsx @@ -0,0 +1,92 @@ +import { Timer } from "./types"; + +export function handleUndefinedTimeLength(number: number | undefined) { + if (number) { + return number; + } + return 0; +} + +export function minuteWording(number: number) { + return "minute".concat(number > 1 ? "s" : ""); +} + +export function generateHtmlInstructions(formTimers: Timer[] | undefined) { + if (!formTimers || !formTimers[0]) { + return undefined; + } + + const elements = [ + + {formTimers[0].timeName} goes in first for{" "} + + {formTimers[0].timeLength}{" "} + {minuteWording( + handleUndefinedTimeLength(formTimers[0].timeLength) + )} + + , + ]; + for (let i = 1; i < formTimers.length; i++) { + if (formTimers[i].timeAfter === 0) { + elements.push( + + {formTimers[i].timeName} starts at the{" "} + same time as{" "} + {formTimers[i - 1].timeName} + + ); + } else { + elements.push( + + {formTimers[i].timeName} starts{" "} + + {formTimers[i].timeAfter}{" "} + {minuteWording(formTimers[i].timeAfter)} + {" "} + after {formTimers[i - 1].timeName} and goes in for{" "} + + {formTimers[i].timeLength}{" "} + {minuteWording( + handleUndefinedTimeLength(formTimers[i].timeLength) + )} + + + ); + } + } + return elements; +} +export function generatePlainTextInstructions(formTimers: Timer[] | undefined) { + if (!formTimers || !formTimers[0]) { + return undefined; + } + + const elements = [ + `1. ${formTimers[0].timeName} goes in first for ${ + formTimers[0].timeLength + } ${minuteWording( + handleUndefinedTimeLength(formTimers[0].timeLength) + )}`, + ]; + for (let i = 1; i < formTimers.length; i++) { + if (formTimers[i].timeAfter === 0) { + elements.push( + `${i + 1}. ${ + formTimers[i].timeName + } starts at the same time as ${formTimers[i - 1].timeName}` + ); + } else { + elements.push( + `${i + 1}. ${formTimers[i].timeName} starts ${ + formTimers[i].timeAfter + } ${minuteWording(formTimers[i].timeAfter)} after ${ + formTimers[i - 1].timeName + } and goes in for ${formTimers[i].timeLength} ${minuteWording( + handleUndefinedTimeLength(formTimers[i].timeLength) + )}` + ); + } + } + return elements; +} From 9d15eb9dfcac6e9212e76307aaf1a5d43f3d5a4f Mon Sep 17 00:00:00 2001 From: Matt George Date: Mon, 18 Dec 2023 22:36:57 +0000 Subject: [PATCH 04/21] Add mocks to fix the tests, added some todos --- src/App.test.tsx | 11 +++++++++++ src/App.tsx | 14 +++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index d8a746e..a116756 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,6 +1,17 @@ import { fireEvent, render, waitFor } from "@testing-library/react"; import { App } from "./App"; +const mockGetItem = jest.fn(); +const mockSetItem = jest.fn(); +const mockRemoveItem = jest.fn(); +Object.defineProperty(window, "localStorage", { + value: { + getItem: (...args: string[]) => mockGetItem(...args), + setItem: (...args: string[]) => mockSetItem(...args), + removeItem: (...args: string[]) => mockRemoveItem(...args), + }, +}); + describe("", () => { test("renders the basic form", () => { const subject = render(); diff --git a/src/App.tsx b/src/App.tsx index 9517650..05748e4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,12 @@ import { handleUndefinedTimeLength, } from "./utils"; +// TODO +// Test the functions individually +// Test the local storage stuff (mocks) +// Need to mock the localstorage to make the test work correctly +// Fix the formik test warnings + export function App() { const [storedTimers, setStoredTimers] = useState( window.localStorage.getItem("timers") @@ -22,12 +28,11 @@ export function App() { useState(); useEffect(() => { - const timers = window.localStorage.getItem("timers"); - if (timers) { + if (storedTimers) { setPlainTextInstructions( - generatePlainTextInstructions(JSON.parse(timers)) + generatePlainTextInstructions(JSON.parse(storedTimers)) ); - setInstructions(generateHtmlInstructions(JSON.parse(timers))); + setInstructions(generateHtmlInstructions(JSON.parse(storedTimers))); } }, [storedTimers]); @@ -39,7 +44,6 @@ export function App() { const initialValues: Values = { timers: storedTimers ? JSON.parse(storedTimers) : [emptyTimer], }; - console.log({ storedTimers }); const handleSubmit = useCallback((values: Values) => { const formattedTimers = values.timers From d9ae5a27a0ecf0fd1ce9282d7dea6bdc69abcbdc Mon Sep 17 00:00:00 2001 From: Matt George Date: Mon, 18 Dec 2023 22:39:25 +0000 Subject: [PATCH 05/21] Move todos to PR --- src/App.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 05748e4..f5ecfbd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,12 +12,6 @@ import { handleUndefinedTimeLength, } from "./utils"; -// TODO -// Test the functions individually -// Test the local storage stuff (mocks) -// Need to mock the localstorage to make the test work correctly -// Fix the formik test warnings - export function App() { const [storedTimers, setStoredTimers] = useState( window.localStorage.getItem("timers") From 575d2c2564e007257cf6cd627e7d3534f3e9327a Mon Sep 17 00:00:00 2001 From: Matt George Date: Sat, 23 Dec 2023 09:07:09 +0000 Subject: [PATCH 06/21] Add reset form button --- src/App.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f5ecfbd..44f09d4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -66,6 +66,11 @@ export function App() { window.localStorage.setItem("timers", JSON.stringify(formattedTimers)); }, []); + const handleReset = () => { + window.localStorage.removeItem("timers"); + window.location.reload(); + }; + return ( <>
@@ -75,7 +80,7 @@ export function App() { initialValues={initialValues} onSubmit={handleSubmit} > - {({ values }) => ( + {({ values, dirty }) => (
@@ -127,11 +132,20 @@ export function App() { Add Timer + {(dirty || storedTimers) && ( + + )}
)} From e002d1b3c2a1c9e111c534e4155718a96beacb01 Mon Sep 17 00:00:00 2001 From: Matt George Date: Sat, 23 Dec 2023 09:46:59 +0000 Subject: [PATCH 07/21] Update linting rules --- .env | 1 + .eslintrc.cjs | 28 +--- package.json | 5 + src/Contact.tsx | 2 +- src/index.tsx | 1 + yarn.lock | 352 ++++++++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 349 insertions(+), 40 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..ea1372f --- /dev/null +++ b/.env @@ -0,0 +1 @@ +ESLINT_NO_DEV_ERRORS=true \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 52e7269..21cb608 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,29 +1,5 @@ module.exports = { root: true, - env: { browser: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - "react-app", - "react-app/jest", - ], - ignorePatterns: ["dist", ".eslintrc.cjs", "jest.config.ts"], - parser: "@typescript-eslint/parser", - plugins: ["react"], - extends: ["eslint:recommended", "plugin:react/recommended"], - rules: { - "react/react-in-jsx-scope": 0, - "react/jsx-uses-react": 0, - "react/no-unescaped-entities": 0, - "no-undef": 0, - }, - overrides: [ - { - files: ["**/__helpers__/**", "**/*.test.*"], - env: { - jest: true, - }, - }, - ], + rules: { "prettier/prettier": 0 }, + extends: ["universe/web", "plugin:jsx-a11y/recommended"], }; diff --git a/package.json b/package.json index f4c3290..9490d17 100644 --- a/package.json +++ b/package.json @@ -43,5 +43,10 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "eslint": "^8.56.0", + "eslint-config-universe": "^12.0.0", + "prettier": "^3.1.1" } } diff --git a/src/Contact.tsx b/src/Contact.tsx index e13f388..1537726 100644 --- a/src/Contact.tsx +++ b/src/Contact.tsx @@ -1,6 +1,6 @@ export const Contact = () => { return ( -
+
Date: Sat, 23 Dec 2023 09:47:27 +0000 Subject: [PATCH 08/21] Create empty advancedmode component --- src/AdvancedMode.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/AdvancedMode.tsx diff --git a/src/AdvancedMode.tsx b/src/AdvancedMode.tsx new file mode 100644 index 0000000..5bee3cc --- /dev/null +++ b/src/AdvancedMode.tsx @@ -0,0 +1,20 @@ +export async function AdvancedMode() { + // Render a simple formik form that has a single text box + // When the user submits that form you can do something like this + // const handleSubmit = (values: Values) => { + // const response = await fetch(url, { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ timer data }), + // }); + // const responseData = await response.json(); + // setResponse(responseData); + // }; + + // The idea is it's going to be a fire and forget + // Maybe we do want to catch the response in case it is successful? + + return <>; +} From ffcd8e9a232f32b5851f0b6fd2be909613de2880 Mon Sep 17 00:00:00 2001 From: Matt George Date: Sat, 23 Dec 2023 09:47:41 +0000 Subject: [PATCH 09/21] Split function off into utils file --- src/utils.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/utils.tsx b/src/utils.tsx index 1a1e889..383fd8d 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -90,3 +90,26 @@ export function generatePlainTextInstructions(formTimers: Timer[] | undefined) { } return elements; } + +export function formatTimers(timers: Timer[]) { + const formattedTimers = timers + .slice() + .filter((timer) => handleUndefinedTimeLength(timer.timeLength) > 0) + .filter((timer) => timer.timeName) + .sort(function (a, b) { + return ( + handleUndefinedTimeLength(a.timeLength) - + handleUndefinedTimeLength(b.timeLength) + ); + }) + .reverse(); + if (formattedTimers.length > 1) { + for (let i = 1; i < formattedTimers.length; i++) { + formattedTimers[i].timeAfter = Math.abs( + handleUndefinedTimeLength(formattedTimers[i].timeLength) - + handleUndefinedTimeLength(formattedTimers[i - 1].timeLength) + ); + } + } + return formattedTimers; +} From 5afedfc89700ab38a9854e07e501722ca6254263 Mon Sep 17 00:00:00 2001 From: Matt George Date: Sat, 23 Dec 2023 09:47:56 +0000 Subject: [PATCH 10/21] Move timer row into it's own component --- src/TimerRow.tsx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/TimerRow.tsx diff --git a/src/TimerRow.tsx b/src/TimerRow.tsx new file mode 100644 index 0000000..50dddb5 --- /dev/null +++ b/src/TimerRow.tsx @@ -0,0 +1,26 @@ +import { CookingTimeField } from "./CookingTimeField"; +import { FoodItemField } from "./FoodItemField"; + +interface Props { + index: number; + removeTimer: (index: number) => void | undefined; +} + +export function TimerRow(props: Props) { + const { index, removeTimer } = props; + return ( +
+ + + +
+ ); +} From d5f9e2a3b3aeb09431649d8f385af447a5af74b9 Mon Sep 17 00:00:00 2001 From: Matt George Date: Sat, 23 Dec 2023 09:48:18 +0000 Subject: [PATCH 11/21] Move code out of the component to make it easier to read --- src/App.tsx | 78 +++++++++++++---------------------------------------- 1 file changed, 19 insertions(+), 59 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 44f09d4..c869d4c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,26 +1,27 @@ import { ArrayHelpers, FieldArray, Form, Formik } from "formik"; import { useCallback, useEffect, useState } from "react"; + import { Contact } from "./Contact"; -import { CookingTimeField } from "./CookingTimeField"; -import { FoodItemField } from "./FoodItemField"; import { Heading } from "./Heading"; import { Instructions } from "./Instructions"; +import { TimerRow } from "./TimerRow"; import { Values } from "./types"; import { + formatTimers, generateHtmlInstructions, generatePlainTextInstructions, - handleUndefinedTimeLength, } from "./utils"; export function App() { const [storedTimers, setStoredTimers] = useState( window.localStorage.getItem("timers") ); - const [instructions, setInstructions] = useState(); const [plainTextInstructions, setPlainTextInstructions] = useState(); + const advancedMode = window.location.search.toLowerCase() === "advanced"; + useEffect(() => { if (storedTimers) { setPlainTextInstructions( @@ -40,27 +41,7 @@ export function App() { }; const handleSubmit = useCallback((values: Values) => { - const formattedTimers = values.timers - .slice() - .filter((timer) => handleUndefinedTimeLength(timer.timeLength) > 0) - .filter((timer) => timer.timeName) - .sort(function (a, b) { - return ( - handleUndefinedTimeLength(a.timeLength) - - handleUndefinedTimeLength(b.timeLength) - ); - }) - .reverse(); - if (formattedTimers.length > 1) { - for (let i = 1; i < formattedTimers.length; i++) { - formattedTimers[i].timeAfter = Math.abs( - handleUndefinedTimeLength(formattedTimers[i].timeLength) - - handleUndefinedTimeLength( - formattedTimers[i - 1].timeLength - ) - ); - } - } + const formattedTimers = formatTimers(values.timers); setStoredTimers(JSON.stringify(formattedTimers)); window.localStorage.setItem("timers", JSON.stringify(formattedTimers)); @@ -89,37 +70,13 @@ export function App() { {values.timers.length > 0 && values.timers.map( (timer, index) => ( -
- - - -
+ index={index} + removeTimer={ + remove + } + /> ) )}
{instructions && plainTextInstructions && ( - + <> + + {advancedMode &&
} + )}
From f57328b0950d899cb0d26350bcd0f1afe45eaa1e Mon Sep 17 00:00:00 2001 From: Matt George Date: Sat, 23 Dec 2023 10:04:38 +0000 Subject: [PATCH 12/21] Fix test warnings --- src/App.test.tsx | 32 +++++++++++++++++++++++--------- src/App.tsx | 2 +- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index a116756..57dd167 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,4 +1,6 @@ -import { fireEvent, render, waitFor } from "@testing-library/react"; +import { act, fireEvent, render, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; + import { App } from "./App"; const mockGetItem = jest.fn(); @@ -33,7 +35,11 @@ describe("", () => { expect(subject.getAllByRole("textbox")).toHaveLength(1); expect(subject.getAllByRole("spinbutton")).toHaveLength(1); - fireEvent.click(subject.getByText(/add timer/i)); + // fireEvent.click(subject.getByText(/add timer/i)); + // // Use act when interacting with your component + await act(async () => { + fireEvent.click(subject.getByText(/add timer/i)); + }); await waitFor(() => { expect(subject.getAllByRole("textbox")).toHaveLength(2); @@ -44,13 +50,17 @@ describe("", () => { test("removes fields when button is pressed", async () => { const subject = render(); - fireEvent.click(subject.getByText(/add timer/i)); + await act(async () => { + fireEvent.click(subject.getByText(/add timer/i)); + }); await waitFor(() => { expect(subject.getAllByRole("textbox")).toHaveLength(2); }); expect(subject.getAllByRole("spinbutton")).toHaveLength(2); - fireEvent.click(subject.getAllByText(/clear/i)[1]); + await act(async () => { + fireEvent.click(subject.getAllByText(/clear/i)[1]); + }); await waitFor(() => { expect(subject.getAllByRole("textbox")).toHaveLength(1); }); @@ -60,8 +70,13 @@ describe("", () => { test("renders the instructions when go is pressed", async () => { const subject = render(); - fireEvent.click(subject.getByText(/add timer/i)); - fireEvent.click(subject.getByText(/add timer/i)); + await act(async () => { + fireEvent.click(subject.getByText(/add timer/i)); + }); + + await act(async () => { + fireEvent.click(subject.getByText(/add timer/i)); + }); await waitFor(() => { expect(subject.getAllByRole("textbox")).toHaveLength(3); }); @@ -73,9 +88,8 @@ describe("", () => { fireEvent.change(timerNameFields[0], { target: { value: "Food A" } }); fireEvent.change(timerNameFields[1], { target: { value: "Food B" } }); fireEvent.change(timerNameFields[2], { target: { value: "Food C" } }); - await waitFor(() => { - fireEvent.change(timerLengthFields[0], { target: { value: 10 } }); - }); + + fireEvent.change(timerLengthFields[0], { target: { value: 10 } }); fireEvent.change(timerLengthFields[1], { target: { value: 8 } }); fireEvent.change(timerLengthFields[2], { target: { value: 5 } }); diff --git a/src/App.tsx b/src/App.tsx index c869d4c..15409be 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,7 +33,7 @@ export function App() { const emptyTimer = { timeName: "", - timeLength: undefined, + timeLength: "", timeAfter: 0, }; const initialValues: Values = { From d15e396256261e75763cbf5cdd0a2499419c1094 Mon Sep 17 00:00:00 2001 From: Matt George Date: Sat, 23 Dec 2023 16:41:00 +0000 Subject: [PATCH 13/21] Remove import --- src/App.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 57dd167..e42a22b 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -1,5 +1,4 @@ import { act, fireEvent, render, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; import { App } from "./App"; From e78fd14ace6b46f25d9dd9c26a15a0374e00281d Mon Sep 17 00:00:00 2001 From: Matt George Date: Sat, 23 Dec 2023 17:32:59 +0000 Subject: [PATCH 14/21] Get instructions component to handle formatting the data also added auto scroll --- src/App.tsx | 49 ++++++++++++++++++++------------------------ src/Instructions.tsx | 18 ++++++++++++---- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 15409be..cc58c1b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,35 +1,25 @@ +import { time } from "console"; import { ArrayHelpers, FieldArray, Form, Formik } from "formik"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { AdvancedMode } from "./AdvancedMode"; import { Contact } from "./Contact"; import { Heading } from "./Heading"; import { Instructions } from "./Instructions"; import { TimerRow } from "./TimerRow"; -import { Values } from "./types"; -import { - formatTimers, - generateHtmlInstructions, - generatePlainTextInstructions, -} from "./utils"; +import { Timer, Values } from "./types"; +import { formatTimers } from "./utils"; export function App() { + const instructionsRef = useRef(null); const [storedTimers, setStoredTimers] = useState( window.localStorage.getItem("timers") ); - const [instructions, setInstructions] = useState(); - const [plainTextInstructions, setPlainTextInstructions] = - useState(); - const advancedMode = window.location.search.toLowerCase() === "advanced"; - - useEffect(() => { - if (storedTimers) { - setPlainTextInstructions( - generatePlainTextInstructions(JSON.parse(storedTimers)) - ); - setInstructions(generateHtmlInstructions(JSON.parse(storedTimers))); - } - }, [storedTimers]); + const [timerData, setTimerData] = useState( + storedTimers && JSON.parse(storedTimers) + ); + const advancedMode = window.location.pathname.toLowerCase() === "/advanced"; const emptyTimer = { timeName: "", @@ -42,9 +32,10 @@ export function App() { const handleSubmit = useCallback((values: Values) => { const formattedTimers = formatTimers(values.timers); - + setTimerData(formattedTimers); setStoredTimers(JSON.stringify(formattedTimers)); window.localStorage.setItem("timers", JSON.stringify(formattedTimers)); + instructionsRef.current?.scrollIntoView(); }, []); const handleReset = () => { @@ -111,17 +102,21 @@ export function App() { )}
- {instructions && plainTextInstructions && ( - <> + {timerData && ( +
- {advancedMode &&
} - + {advancedMode && timerData && ( + + )} +
)}
); } + +// Maybe a mini border between form rows diff --git a/src/Instructions.tsx b/src/Instructions.tsx index 0e6b814..fedc588 100644 --- a/src/Instructions.tsx +++ b/src/Instructions.tsx @@ -1,12 +1,21 @@ import { useCallback, useState } from "react"; +import { Timer } from "./types"; +import { + generateHtmlInstructions, + generatePlainTextInstructions, +} from "./utils"; + interface Props { - instructions: JSX.Element[]; - plainTextInstructions: string[]; + timerData: Timer[]; + buttonRef: React.RefObject; } export const Instructions = (props: Props) => { - const { instructions, plainTextInstructions } = props; + const plainTextInstructions = generatePlainTextInstructions( + props.timerData + ); + const htmlInstructions = generateHtmlInstructions(props.timerData); const copyClipboardWordingDefault = "Copy to Clipboard"; const [copyClipboardWording, setCopyClipboardWording] = useState( @@ -33,7 +42,7 @@ export const Instructions = (props: Props) => { return (