├── .eslintrc.yaml ├── models ├── localStorage-keys.js └── database.js ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── site.webmanifest └── about.txt ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── .prettierignore ├── .gitignore ├── components ├── DeleteAllCompletedButton.jsx ├── ShowCompletedTasksSwitch.jsx ├── DeleteButton.jsx ├── LanguageSelect.jsx ├── Layout.jsx ├── TodoList.jsx ├── TaskCountSettings.jsx ├── TodoItem.jsx └── TodoInput.jsx ├── pages ├── _app.jsx ├── _document.jsx ├── settings.jsx └── index.jsx ├── localization.json ├── package.json ├── LICENSE ├── scripts ├── lib.mjs ├── checkpoint-rebase-bubble.mjs └── checkpoint-rename-bubble.mjs ├── contexts ├── localization.jsx └── settings.jsx ├── README.md └── next.config.js /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - eslint:recommended 3 | - next/core-web-vitals 4 | - prettier 5 | -------------------------------------------------------------------------------- /models/localStorage-keys.js: -------------------------------------------------------------------------------- 1 | export const SAVED_INPUT = "savedInput"; 2 | export const SETTINGS = "settings"; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "divlo.vscode-styled-jsx-languageserver", 5 | "divlo.vscode-styled-jsx-syntax", 6 | "esbenp.prettier-vscode", 7 | "streetsidesoftware.code-spell-checker" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /public/about.txt: -------------------------------------------------------------------------------- 1 | Favicon graphic courtesy Twemoji v13.1 2 | Copyright 2020 Twitter, Inc and other contributors 3 | Licensed under CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/) 4 | Source: https://github.com/twitter/twemoji/blob/ff403353b5882d6d7037e289c7dc98e9b9747e2b/assets/svg/1fa9d.svg 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "animejs", 4 | "architecting", 5 | "clsx", 6 | "Dexie", 7 | "fontawesome", 8 | "fortawesome", 9 | "lrgb", 10 | "Packt", 11 | "Vercel" 12 | ], 13 | "editor.defaultFormatter": "esbenp.prettier-vscode", 14 | "files.associations": { 15 | "*.jsx": "javascriptreact" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.webmanifest 2 | 3 | # Next.js recommended .gitignore 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | sandbox 2 | *.code-workspace 3 | 4 | # Next.js recommended .gitignore 5 | 6 | # dependencies 7 | /node_modules 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # vercel 37 | .vercel 38 | -------------------------------------------------------------------------------- /components/DeleteAllCompletedButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Button } from "react-bootstrap"; 3 | import { LocalizationContext } from "../contexts/localization"; 4 | import { deleteAllCompletedTasks } from "../models/database"; 5 | 6 | export default function DeleteAllCompletedButton(props) { 7 | const localizedStrings = useContext(LocalizationContext); 8 | 9 | return ( 10 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /pages/_app.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SettingsProvider } from "../contexts/settings"; 3 | import { LocalizationProvider } from "../contexts/localization"; 4 | import Layout from "../components/Layout"; 5 | import "bootstrap/dist/css/bootstrap.min.css"; 6 | 7 | export default function App({ Component, pageProps }) { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/ShowCompletedTasksSwitch.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Form } from "react-bootstrap"; 3 | import { LocalizationContext } from "../contexts/localization"; 4 | 5 | function ShowCompletedTasksSwitch({ value, toggle, ...props }) { 6 | const localizedStrings = useContext(LocalizationContext); 7 | 8 | return ( 9 |
10 | 17 |
18 | ); 19 | } 20 | export default React.memo(ShowCompletedTasksSwitch); 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "npm run dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "pwa-chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Next.js: debug full stack", 18 | "type": "node-terminal", 19 | "request": "launch", 20 | "command": "npm run dev", 21 | "console": "integratedTerminal", 22 | "serverReadyAction": { 23 | "pattern": "started server on .+, url: (https?://.+)", 24 | "uriFormat": "%s", 25 | "action": "debugWithChrome" 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /components/DeleteButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Button } from "react-bootstrap"; 3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 4 | import { faTrash } from "@fortawesome/free-solid-svg-icons"; 5 | import clsx from "clsx"; 6 | import { merge } from "lodash"; 7 | import { LocalizationContext } from "../contexts/localization"; 8 | 9 | export default function DeleteButton({ visible, style, className, ...props }) { 10 | const localizedStrings = useContext(LocalizationContext); 11 | 12 | return ( 13 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /localization.json: -------------------------------------------------------------------------------- 1 | { 2 | "en-US": { 3 | "add": "Add", 4 | "badTaskCount": "Bad task count:", 5 | "deleteAllCompleted": "Delete all completed tasks", 6 | "deleteTask": "Delete task", 7 | "goodTaskCount": "Good task count:", 8 | "home": "Home", 9 | "inputLabel": "To-do item input", 10 | "language": { 11 | "label": "Language:", 12 | "en-US": "English", 13 | "tlh": "Klingon" 14 | }, 15 | "projectTitle": "To-Do List Project", 16 | "settings": "Settings", 17 | "showCompletedTasks": "Show completed tasks" 18 | }, 19 | "tlh": { 20 | "add": "boq", 21 | "badTaskCount": "'ach HeSwI''e' je.", 22 | "deleteAllCompleted": "ghotvam ghotvam", 23 | "deleteTask": "Dov'a'", 24 | "goodTaskCount": "vaj DataHvIS.", 25 | "home": "juH", 26 | "inputLabel": "'e' yIlIjQo', ghaH'e'", 27 | "language": { 28 | "label": "Hol:", 29 | "en-US": "Te ra'", 30 | "tlh": "tlhIngan" 31 | }, 32 | "projectTitle": "DaytuqQo'qoq", 33 | "settings": "'aw'", 34 | "showCompletedTasks": "yIjatlhqa', qoHlu'DI'" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pages/_document.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Document, { Html, Head, Main, NextScript } from "next/document"; 3 | 4 | export default class MyDocument extends Document { 5 | render() { 6 | return ( 7 | 8 | {/* prettier-ignore */} 9 | 10 | {/* Favicon graphic courtesy Twemoji v13.1 11 | Copyright 2020 Twitter, Inc and other contributors 12 | Licensed under CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/) 13 | Source: https://github.com/twitter/twemoji/blob/ff403353b5882d6d7037e289c7dc98e9b9747e2b/assets/svg/1fa9d.svg */} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-list-project", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint", 9 | "deploy": "vercel --prod", 10 | "rebase-bubble": "node scripts/checkpoint-rebase-bubble.mjs", 11 | "rename-bubble": "node scripts/checkpoint-rename-bubble.mjs" 12 | }, 13 | "dependencies": { 14 | "@fortawesome/fontawesome-svg-core": "^1.2.36", 15 | "@fortawesome/free-solid-svg-icons": "^5.15.4", 16 | "@fortawesome/react-fontawesome": "^0.1.15", 17 | "animejs": "^3.2.1", 18 | "bootstrap": "^5.1.3", 19 | "chroma-js": "^2.1.2", 20 | "clsx": "^1.1.1", 21 | "dexie": "^3.2.0-rc.1", 22 | "immutable": "^4.0.0", 23 | "lodash": "^4.17.21", 24 | "next": "^12.0.2", 25 | "react": "^17.0.2", 26 | "react-bootstrap": "^2.0.0", 27 | "react-dom": "^17.0.2" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^7.32.0", 31 | "eslint-config-next": "^12.0.2", 32 | "eslint-config-prettier": "^8.3.0", 33 | "prettier": "^2.3.1", 34 | "simple-git": "^2.47.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tyler Westin Mick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/lib.mjs: -------------------------------------------------------------------------------- 1 | import { Seq } from "immutable"; 2 | 3 | export async function getCheckpointBranches(git) { 4 | const branchSummary = await git.branch(["--list"]); 5 | 6 | let currentBranch; 7 | const branches = Seq(branchSummary.all) 8 | .filter((branch) => branch.startsWith("Checkpoint_")) 9 | .map((branchName) => { 10 | const { chapter, decimal } = branchName.match( 11 | /Checkpoint_(?\d+)\.(?\d+)/ 12 | ).groups; 13 | const branch = { 14 | name: branchName, 15 | chapter: parseInt(chapter), 16 | decimal: parseInt(decimal), 17 | }; 18 | if (branchName === branchSummary.current) { 19 | currentBranch = branch; 20 | } 21 | return branch; 22 | }) 23 | .sort( 24 | ( 25 | { chapter: chapterA, decimal: decimalA }, 26 | { chapter: chapterB, decimal: decimalB } 27 | ) => { 28 | if (chapterA === chapterB) return decimalA - decimalB; 29 | else return chapterA - chapterB; 30 | } 31 | ) 32 | .concat({ name: "main" }) 33 | .toJS(); 34 | 35 | return { branches, currentBranch }; 36 | } 37 | -------------------------------------------------------------------------------- /models/database.js: -------------------------------------------------------------------------------- 1 | import Dexie, { liveQuery } from "dexie"; 2 | 3 | // Dexie `where` queries don't accept booleans 4 | const FALSE = 0; 5 | const TRUE = 1; 6 | 7 | const db = new Dexie("TodoList"); 8 | 9 | db.version(3).stores({ 10 | tasks: "++id, description, completed", 11 | }); 12 | 13 | export default db; 14 | 15 | export const addTask = async (description) => 16 | await db.tasks.add({ description, completed: FALSE }); 17 | 18 | export const deleteTask = async (id) => await db.tasks.delete(id); 19 | 20 | export const toggleTaskCompleted = async (id, prevCompleted) => 21 | await db.tasks.update(id, { completed: prevCompleted ? FALSE : TRUE }); 22 | 23 | export const getAllTasks = async () => 24 | (await db.tasks.toArray()).map((task) => ({ 25 | ...task, 26 | completed: task.completed === TRUE, 27 | })); 28 | 29 | export function subscribeToTaskList(onUpdate) { 30 | const taskListObservable = liveQuery(getAllTasks); 31 | const subscription = taskListObservable.subscribe({ 32 | next: (tasks) => onUpdate(tasks), 33 | error: (error) => console.error(error), 34 | }); 35 | return subscription.unsubscribe; 36 | } 37 | 38 | export const deleteAllCompletedTasks = async () => 39 | await db.tasks.where("completed").equals(TRUE).delete(); 40 | -------------------------------------------------------------------------------- /pages/settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { Form } from "react-bootstrap"; 3 | import { LocalizationContext } from "../contexts/localization"; 4 | import LanguageSelect from "../components/LanguageSelect"; 5 | import { SettingsContext } from "../contexts/settings"; 6 | import TaskCountSettings from "../components/TaskCountSettings"; 7 | 8 | const formStyle = Object.freeze({ 9 | display: "grid", 10 | gridTemplateColumns: "max-content min-content", 11 | alignItems: "center", 12 | gap: "1rem", 13 | }); 14 | 15 | export default function Settings() { 16 | const settings = useContext(SettingsContext); 17 | const localizedStrings = useContext(LocalizationContext); 18 | 19 | return ( 20 | <> 21 |

{localizedStrings.settings}

22 | 23 |
24 | 28 | 29 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/LanguageSelect.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useContext } from "react"; 2 | import { Form, ToggleButton, ToggleButtonGroup } from "react-bootstrap"; 3 | import { LocalizationContext } from "../contexts/localization"; 4 | 5 | export default function LanguageSelect({ locale, setLocale }) { 6 | const localizedStrings = useContext(LocalizationContext); 7 | 8 | return ( 9 | 10 | 11 | {localizedStrings.language.label} 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | function LanguageButton({ value, ...props }) { 28 | const localizedStrings = useContext(LocalizationContext); 29 | return ( 30 | 37 | {localizedStrings.language[value]} 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import Link from "next/link"; 3 | import { useRouter } from "next/router"; 4 | import { Container, Nav, Navbar } from "react-bootstrap"; 5 | import { LocalizationContext } from "../contexts/localization"; 6 | 7 | export default function Layout({ children }) { 8 | const { pathname } = useRouter(); 9 | const localizedStrings = useContext(LocalizationContext); 10 | 11 | return ( 12 | <> 13 | 18 | 19 | 30 | 31 | 32 | 33 | 34 | {children} 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /scripts/checkpoint-rebase-bubble.mjs: -------------------------------------------------------------------------------- 1 | import simpleGit from "simple-git"; 2 | import { getCheckpointBranches } from "./lib.mjs"; 3 | 4 | const git = simpleGit(); 5 | 6 | const options = { 7 | interactive: 8 | process.argv.includes("--interactive") || process.argv.includes("-i"), 9 | }; 10 | 11 | (async function () { 12 | let previousBranch; 13 | try { 14 | const { branches, currentBranch: startingBranch } = 15 | await getCheckpointBranches(git); 16 | const branchNames = branches.map((branch) => branch.name); 17 | 18 | const startingBranchIndex = branchNames.indexOf(startingBranch.name); 19 | previousBranch = branchNames[startingBranchIndex]; 20 | for (let i = startingBranchIndex + 1; i < branchNames.length; i++) { 21 | const currentBranch = branchNames[i]; 22 | 23 | await git.checkout(currentBranch).rebase({ 24 | ...(options.interactive ? { "--interactive": null } : {}), 25 | [previousBranch]: null, 26 | }); 27 | 28 | previousBranch = currentBranch; 29 | } 30 | 31 | git.push(["--all", "--set-upstream", "origin", "--force-with-lease"]); 32 | } catch (error) { 33 | console.error(error.message); 34 | process.exitCode = 1; 35 | await git.rebase(["--abort"]).checkout(previousBranch); 36 | } 37 | })(); 38 | -------------------------------------------------------------------------------- /scripts/checkpoint-rename-bubble.mjs: -------------------------------------------------------------------------------- 1 | import simpleGit from "simple-git"; 2 | import { getCheckpointBranches } from "./lib.mjs"; 3 | 4 | const git = simpleGit(); 5 | 6 | (async function () { 7 | try { 8 | const { branches, currentBranch } = await getCheckpointBranches(git); 9 | const branchesToRename = branches 10 | .filter( 11 | (branch) => 12 | branch.chapter === currentBranch.chapter && 13 | branch.decimal >= currentBranch.decimal 14 | ) 15 | .reverse(); 16 | 17 | const deleteRemoteBranchTasks = branchesToRename.map((branch) => 18 | git.push("origin", branch.name, ["--delete"]) 19 | ); 20 | 21 | for (const branch of branchesToRename) { 22 | const isCurrentBranch = branch.name === currentBranch.name; 23 | const newName = `Checkpoint_${branch.chapter}.${branch.decimal + 1}`; 24 | await git.branch([ 25 | "--move", 26 | ...(isCurrentBranch ? [] : [branch.name]), 27 | newName, 28 | ]); 29 | } 30 | await git.checkoutLocalBranch(currentBranch.name); 31 | 32 | await Promise.all(deleteRemoteBranchTasks); 33 | 34 | git.push(["--all", "--set-upstream", "origin", "--force-with-lease"]); 35 | } catch (error) { 36 | console.error(error.message); 37 | process.exitCode = 1; 38 | } 39 | })(); 40 | -------------------------------------------------------------------------------- /contexts/localization.jsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useContext, 4 | useLayoutEffect, 5 | useState, 6 | } from "react"; 7 | import { SettingsContext } from "./settings"; 8 | import localization from "../localization.json"; 9 | 10 | const supportedLocales = Object.keys(localization); 11 | 12 | export const LocalizationContext = createContext(); 13 | 14 | export function LocalizationProvider({ children }) { 15 | const settings = useContext(SettingsContext); 16 | 17 | const [closestUserLocale, setClosestUserLocale] = useState("en-US"); 18 | useLayoutEffect(() => { 19 | const userLocales = (navigator?.languages ?? [navigator?.language]).filter( 20 | (language) => language != null 21 | ); 22 | 23 | for (const userLocale of userLocales) { 24 | const userLanguage = userLocale.split("-")[0]; 25 | const resolvedLocale = 26 | supportedLocales.find((locale) => locale === userLocale) ?? 27 | supportedLocales.find((locale) => locale.startsWith(userLanguage)); 28 | 29 | if (resolvedLocale) { 30 | setClosestUserLocale(resolvedLocale); 31 | break; 32 | } 33 | } 34 | }, []); 35 | 36 | const locale = settings.locale ?? closestUserLocale; 37 | 38 | return ( 39 | 40 | {children} 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /contexts/settings.jsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useLayoutEffect, 4 | useMemo, 5 | useState, 6 | } from "react"; 7 | import { merge } from "immutable"; 8 | import { SETTINGS } from "../models/localStorage-keys"; 9 | 10 | export const SettingsContext = createContext(); 11 | 12 | const defaultSettings = Object.freeze({ 13 | locale: null, 14 | goodTaskCount: 3, 15 | badTaskCount: 10, 16 | }); 17 | 18 | export function SettingsProvider({ children }) { 19 | const [settings, setSettings] = useState(defaultSettings); 20 | function updateLocalAndStoredSettings(newSettings) { 21 | setSettings((prevSettings) => { 22 | const mergedSettings = merge(prevSettings, newSettings); 23 | localStorage.setItem(SETTINGS, JSON.stringify(mergedSettings)); 24 | return mergedSettings; 25 | }); 26 | } 27 | 28 | useLayoutEffect(() => { 29 | const settingsJson = localStorage.getItem(SETTINGS); 30 | updateLocalAndStoredSettings(settingsJson ? JSON.parse(settingsJson) : {}); 31 | }, []); 32 | 33 | const setters = useMemo(() => { 34 | const setters = {}; 35 | for (const key in defaultSettings) { 36 | setters[key] = (newValue) => 37 | updateLocalAndStoredSettings({ [key]: newValue }); 38 | } 39 | return setters; 40 | }, []); 41 | 42 | const contextValue = useMemo( 43 | () => ({ ...settings, set: setters }), 44 | [setters, settings] 45 | ); 46 | 47 | return ( 48 | 49 | {children} 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /components/TodoList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import anime from "animejs"; 3 | import TodoItem from "./TodoItem"; 4 | 5 | function TodoList({ tasks, ...containerProps }) { 6 | const container = useRef(null); 7 | const lastCompletedIndex = useRef(null); 8 | const setLastCompletedIndex = (index) => { 9 | lastCompletedIndex.current = index; 10 | }; 11 | const allTasksCompleted = tasks.every((task) => task.completed); 12 | const allTasksPrevCompleted = useRef(null); 13 | useEffect(() => { 14 | if (allTasksPrevCompleted.current === false && allTasksCompleted) { 15 | startWiggleAnimation( 16 | container.current.children, 17 | lastCompletedIndex.current 18 | ); 19 | } 20 | 21 | allTasksPrevCompleted.current = allTasksCompleted; 22 | }, [allTasksCompleted]); 23 | 24 | return ( 25 |
26 | {tasks.map(({ id, description, completed }, index) => ( 27 | setLastCompletedIndex(index)} 32 | > 33 | {description} 34 | 35 | ))} 36 |
37 | ); 38 | } 39 | export default React.memo(TodoList); 40 | 41 | function startWiggleAnimation(targets, startingIndex) { 42 | anime({ 43 | targets, 44 | keyframes: [ 45 | { translateX: 5 }, 46 | { translateX: 0 }, 47 | { translateX: -5 }, 48 | { translateX: 0 }, 49 | ], 50 | easing: "linear", 51 | duration: 200, 52 | delay: anime.stagger(90, { from: startingIndex }), 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /components/TaskCountSettings.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useCallback, useContext } from "react"; 2 | import { Form } from "react-bootstrap"; 3 | import { LocalizationContext } from "../contexts/localization"; 4 | 5 | const MIN_GOOD_COUNT = 0; 6 | const MIN_BAD_COUNT = 1; 7 | 8 | export default function TaskCountSettings({ 9 | goodCount, 10 | setGoodCount, 11 | badCount, 12 | setBadCount, 13 | }) { 14 | const localizedStrings = useContext(LocalizationContext); 15 | 16 | const handleGoodCountChange = useCallback( 17 | (event) => { 18 | const newGoodCount = Math.max( 19 | parseInt(event.target.value), 20 | MIN_GOOD_COUNT 21 | ); 22 | setGoodCount(newGoodCount); 23 | if (newGoodCount >= badCount) setBadCount(newGoodCount + 1); 24 | }, 25 | [badCount, setBadCount, setGoodCount] 26 | ); 27 | const handleBadCountChange = useCallback( 28 | (event) => { 29 | const newBadCount = Math.max(parseInt(event.target.value), MIN_BAD_COUNT); 30 | setBadCount(newBadCount); 31 | if (newBadCount <= goodCount) setGoodCount(newBadCount - 1); 32 | }, 33 | [goodCount, setBadCount, setGoodCount] 34 | ); 35 | 36 | return ( 37 | <> 38 | 39 | 40 | {localizedStrings.goodTaskCount} 41 | 42 | 48 | 49 | 50 | 51 | {localizedStrings.badTaskCount} 52 | 58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Col, Form, Row } from "react-bootstrap"; 3 | import { 4 | deleteTask as dbDeleteTask, 5 | toggleTaskCompleted as dbToggleTaskCompleted, 6 | } from "../models/database"; 7 | import DeleteButton from "./DeleteButton.jsx"; 8 | 9 | export default function TodoItem({ 10 | children, 11 | taskId, 12 | completed, 13 | onTaskCompletion, 14 | ...checkboxProps 15 | }) { 16 | async function toggleCompleted() { 17 | const prevCompleted = completed; 18 | await dbToggleTaskCompleted(taskId, prevCompleted); 19 | if (!prevCompleted) onTaskCompletion(); 20 | } 21 | const deleteTask = async () => await dbDeleteTask(taskId); 22 | 23 | const [focused, setFocused] = useState(false); 24 | const handleFocus = () => setFocused(true); 25 | const handleBlur = () => setFocused(false); 26 | const [hovered, setHovered] = useState(false); 27 | const handleMouseEnter = () => setHovered(true); 28 | const handleMouseLeave = () => setHovered(false); 29 | 30 | return ( 31 | 37 | 38 | 44 | 52 | 58 | {children} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /pages/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useContext, 4 | useEffect, 5 | useMemo, 6 | useState, 7 | } from "react"; 8 | import { subscribeToTaskList } from "../models/database"; 9 | import { LocalizationContext } from "../contexts/localization"; 10 | import ShowCompletedTasksSwitch from "../components/ShowCompletedTasksSwitch"; 11 | import TodoInput from "../components/TodoInput"; 12 | import TodoList from "../components/TodoList"; 13 | import DeleteAllCompletedButton from "../components/DeleteAllCompletedButton"; 14 | 15 | export default function Home() { 16 | const localizedStrings = useContext(LocalizationContext); 17 | 18 | const [{ tasks, dbConnected }, setTasksDbState] = useState({ 19 | tasks: [], 20 | dbConnected: false, 21 | }); 22 | const setTasks = (newTasks) => 23 | setTasksDbState({ tasks: newTasks, dbConnected: true }); 24 | useEffect(() => { 25 | function onDatabaseUpdate(newTasks) { 26 | setTasks(newTasks); 27 | } 28 | const unsubscribe = subscribeToTaskList(onDatabaseUpdate); 29 | 30 | return function cleanup() { 31 | unsubscribe(); 32 | }; 33 | }, []); 34 | 35 | const incompleteTasks = useMemo( 36 | () => tasks.filter((task) => !task.completed), 37 | [tasks] 38 | ); 39 | 40 | useEffect(() => { 41 | document.title = `(${incompleteTasks.length}) ${localizedStrings.projectTitle}`; 42 | }, [incompleteTasks, localizedStrings.projectTitle]); 43 | 44 | const [showCompleted, setShowCompleted] = useState(false); 45 | const toggleShowCompleted = useCallback( 46 | () => setShowCompleted((prev) => !prev), 47 | [] 48 | ); 49 | 50 | const [inputValue, setInputValue] = useState(""); 51 | 52 | return ( 53 | dbConnected && ( 54 | <> 55 |

{localizedStrings.projectTitle}

56 | 57 | 63 | 68 | 72 | 73 | 74 | ) 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /components/TodoInput.jsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useMemo } from "react"; 2 | import { Col, Button, Form, Row } from "react-bootstrap"; 3 | import chroma from "chroma-js"; 4 | import css from "styled-jsx/css"; 5 | import { LocalizationContext } from "../contexts/localization"; 6 | import { SettingsContext } from "../contexts/settings"; 7 | import { addTask as dbAddTask } from "../models/database"; 8 | 9 | const SUCCESS_COLOR = "#28a745"; 10 | const WARNING_COLOR = "#e3a900"; 11 | const DANGER_COLOR = "#dc3545"; 12 | 13 | export default function TodoInput({ 14 | inputValue, 15 | setInputValue, 16 | incompleteTaskCount, 17 | ...props 18 | }) { 19 | const settings = useContext(SettingsContext); 20 | const localizedStrings = useContext(LocalizationContext); 21 | 22 | async function addTask() { 23 | if (inputValue !== "") { 24 | await dbAddTask(inputValue); 25 | setInputValue(""); 26 | } 27 | } 28 | 29 | const addButtonCss = useMemo(() => { 30 | const colorScale = chroma 31 | .scale([SUCCESS_COLOR, WARNING_COLOR, DANGER_COLOR]) 32 | .mode("lrgb") 33 | .domain([settings.goodTaskCount, settings.badTaskCount]); 34 | const baseColor = colorScale(incompleteTaskCount).hex(); 35 | const hoverFocusColor = chroma.mix(baseColor, "black", 0.15).hex(); 36 | const hoverFocusBorderColor = chroma.mix(baseColor, "black", 0.2).hex(); 37 | const focusShadowColor = chroma 38 | .mix(baseColor, "white", 0.15) 39 | .alpha(0.5) 40 | .css(); 41 | return css.resolve` 42 | button { 43 | background-color: ${baseColor}; 44 | border-color: ${baseColor}; 45 | } 46 | button:hover { 47 | background-color: ${hoverFocusColor}; 48 | border-color: ${hoverFocusBorderColor}; 49 | } 50 | button:focus { 51 | background-color: ${hoverFocusColor}; 52 | border-color: ${hoverFocusBorderColor}; 53 | box-shadow: 0 0 0 0.25rem ${focusShadowColor}; 54 | } 55 | `; 56 | }, [incompleteTaskCount, settings.badTaskCount, settings.goodTaskCount]); 57 | 58 | return ( 59 | 60 | 61 | setInputValue(e.target.value)} 65 | onKeyDown={(event) => { 66 | if (event.key === "Enter") addTask(); 67 | }} 68 | /> 69 | 70 | 71 | 72 | 75 | {addButtonCss.styles} 76 | 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # _Architecting Enterprise React Applications with Hooks: Simplify Your Codebase by Reusing Stateful Logic_ 2 | 3 | Being written by [Ty Mick](https://tymick.me), coming 2022 from [Packt Publishing](https://www.packtpub.com). 4 | 5 | ## List of code checkpoints 6 | 7 | ### Chapter 1: State – Remembering the Past 8 | 9 | - [Checkpoint 1.0](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/tree/Checkpoint_1.0) 10 | - [Checkpoint 1.1](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.1/components/TodoInput.jsx#L5-L13) 11 | - [Checkpoint 1.2](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.2/components/TodoInput.jsx#L5-L10) 12 | - [Checkpoint 1.3](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.3/components/TodoItem.jsx#L5-L8) 13 | - [Checkpoint 1.4](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.4/components/TodoItem.jsx#L6-L14) 14 | - [Checkpoint 1.5](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.5/pages/index.jsx) 15 | - [Checkpoint 1.6](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.6%5E..Checkpoint_1.6) 16 | - [Checkpoint 1.7](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.7%5E..Checkpoint_1.7#diff-3bee99af9a07d239b70ae72925c716008bd316cce43dfbafd095229b4d1d34fa) 17 | - [Checkpoint 1.8](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.8/components/TodoItem.jsx#L14-L23) 18 | - [Checkpoint 1.9](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/tree/Checkpoint_1.9) 19 | - [Checkpoint 1.10](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.10/localization.json) 20 | - [Checkpoint 1.11](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.11%5E..Checkpoint_1.11) 21 | - [Checkpoint 1.12](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.12%5E..Checkpoint_1.12) 22 | - [Checkpoint 1.13](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.13%5E..Checkpoint_1.13) 23 | - [Checkpoint 1.14](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.14%5E..Checkpoint_1.14) 24 | - [Checkpoint 1.15](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.15/components/LanguageSelect.jsx) 25 | - [Checkpoint 1.16](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/tree/Checkpoint_1.16) 26 | - [Checkpoint 1.17](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.17/reducers/todoListReducer.js) 27 | - [Checkpoint 1.18](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.17..Checkpoint_1.18) 28 | - [Checkpoint 1.19](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.19%5E..Checkpoint_1.19) 29 | 30 | ### Chapter 2: Side Effects – Reaching Outside of React 31 | 32 | - [Checkpoint 2.0](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/tree/Checkpoint_2.0) 33 | - [Checkpoint 2.1](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.1/pages/index.jsx#L12-L24) 34 | - [Checkpoint 2.2](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.2/pages/index.jsx#L24-L39) 35 | - [Checkpoint 2.3](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.3%5E...Checkpoint_2.3?diff=split) 36 | - [Checkpoint 2.4](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.4%5E..Checkpoint_2.4) 37 | - [Checkpoint 2.5](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.5/components/TodoInput.jsx) 38 | - [Checkpoint 2.6](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.6/contexts/localization.jsx) 39 | - [Checkpoint 2.7](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.7%5E..Checkpoint_2.7) 40 | - [Checkpoint 2.8](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.8%5E..Checkpoint_2.8) 41 | - [Checkpoint 2.9](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.9%5E..Checkpoint_2.9) 42 | - [Checkpoint 2.10](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.10/pages/demo.jsx) 43 | - [Checkpoint 2.11](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.11/pages/index.jsx) 44 | - [Checkpoint 2.12](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.12/components/TodoList.jsx) 45 | - [Checkpoint 2.13](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.12..Checkpoint_2.13) 46 | 47 | ### Chapter 3: Optimization and Debugging 48 | 49 | - [Checkpoint 3.0](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_3.0/components/TodoInput.jsx#L29-L54) 50 | - [Checkpoint 3.1](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_3.1%5E%5E%5E%5E..Checkpoint_3.1) 51 | - [Checkpoint 3.2](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_3.2%5E%5E..Checkpoint_3.2) 52 | - [Checkpoint 3.3](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_3.3/pages/index.jsx#L55) 53 | - [Checkpoint 3.4](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_3.4/components/TaskCountSettings.jsx#L16-L25) 54 | - [Checkpoint 3.5](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_3.4..Checkpoint_3.5) 55 | - [Checkpoint 3.6](https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_3.6/components/TodoInput.jsx#L29-L56) 56 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | eslint: { 3 | dirs: ["components", "contexts", "models", "pages", "scripts"], 4 | }, 5 | async redirects() { 6 | return [ 7 | { 8 | source: "/checkpoint-1.0", 9 | destination: 10 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/tree/Checkpoint_1.0", 11 | permanent: true, 12 | }, 13 | { 14 | source: "/checkpoint-1.1", 15 | destination: 16 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.1/components/TodoInput.jsx#L5-L13", 17 | permanent: true, 18 | }, 19 | { 20 | source: "/checkpoint-1.2", 21 | destination: 22 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.2/components/TodoInput.jsx#L5-L10", 23 | permanent: true, 24 | }, 25 | { 26 | source: "/checkpoint-1.3", 27 | destination: 28 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.3/components/TodoItem.jsx#L5-L8", 29 | permanent: true, 30 | }, 31 | { 32 | source: "/checkpoint-1.4", 33 | destination: 34 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.4/components/TodoItem.jsx#L6-L14", 35 | permanent: true, 36 | }, 37 | { 38 | source: "/checkpoint-1.5", 39 | destination: 40 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.5/pages/index.jsx", 41 | permanent: true, 42 | }, 43 | { 44 | source: "/checkpoint-1.6", 45 | destination: 46 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.6%5E..Checkpoint_1.6", 47 | permanent: true, 48 | }, 49 | { 50 | source: "/checkpoint-1.7", 51 | destination: 52 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.7%5E..Checkpoint_1.7#diff-3bee99af9a07d239b70ae72925c716008bd316cce43dfbafd095229b4d1d34fa", 53 | permanent: true, 54 | }, 55 | { 56 | source: "/checkpoint-1.8", 57 | destination: 58 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.8/components/TodoItem.jsx#L14-L23", 59 | permanent: true, 60 | }, 61 | { 62 | source: "/checkpoint-1.9", 63 | destination: 64 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/tree/Checkpoint_1.9", 65 | permanent: true, 66 | }, 67 | { 68 | source: "/checkpoint-1.10", 69 | destination: 70 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.10/localization.json", 71 | permanent: true, 72 | }, 73 | { 74 | source: "/checkpoint-1.11", 75 | destination: 76 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.11%5E..Checkpoint_1.11", 77 | permanent: true, 78 | }, 79 | { 80 | source: "/checkpoint-1.12", 81 | destination: 82 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.12%5E..Checkpoint_1.12", 83 | permanent: true, 84 | }, 85 | { 86 | source: "/checkpoint-1.13", 87 | destination: 88 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.13%5E..Checkpoint_1.13", 89 | permanent: true, 90 | }, 91 | { 92 | source: "/checkpoint-1.14", 93 | destination: 94 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.14%5E..Checkpoint_1.14", 95 | permanent: true, 96 | }, 97 | { 98 | source: "/checkpoint-1.15", 99 | destination: 100 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.15/components/LanguageSelect.jsx", 101 | permanent: true, 102 | }, 103 | { 104 | source: "/checkpoint-1.16", 105 | destination: 106 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/tree/Checkpoint_1.16", 107 | permanent: true, 108 | }, 109 | { 110 | source: "/checkpoint-1.17", 111 | destination: 112 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_1.17/reducers/todoListReducer.js", 113 | permanent: true, 114 | }, 115 | { 116 | source: "/checkpoint-1.18", 117 | destination: 118 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.17..Checkpoint_1.18", 119 | permanent: true, 120 | }, 121 | { 122 | source: "/checkpoint-1.19", 123 | destination: 124 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_1.19%5E..Checkpoint_1.19", 125 | permanent: true, 126 | }, 127 | { 128 | source: "/checkpoint-2.0", 129 | destination: 130 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/tree/Checkpoint_2.0", 131 | permanent: true, 132 | }, 133 | { 134 | source: "/checkpoint-2.1", 135 | destination: 136 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.1/pages/index.jsx#L12-L24", 137 | permanent: true, 138 | }, 139 | { 140 | source: "/checkpoint-2.2", 141 | destination: 142 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.2/pages/index.jsx#L24-L39", 143 | permanent: true, 144 | }, 145 | { 146 | source: "/checkpoint-2.3", 147 | destination: 148 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.3%5E...Checkpoint_2.3?diff=split", 149 | permanent: true, 150 | }, 151 | { 152 | source: "/checkpoint-2.4", 153 | destination: 154 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.4%5E..Checkpoint_2.4", 155 | permanent: true, 156 | }, 157 | { 158 | source: "/checkpoint-2.5", 159 | destination: 160 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.5/components/TodoInput.jsx", 161 | permanent: true, 162 | }, 163 | { 164 | source: "/checkpoint-2.6", 165 | destination: 166 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.6/contexts/localization.jsx", 167 | permanent: true, 168 | }, 169 | { 170 | source: "/checkpoint-2.7", 171 | destination: 172 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.7%5E..Checkpoint_2.7", 173 | permanent: true, 174 | }, 175 | { 176 | source: "/checkpoint-2.8", 177 | destination: 178 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.8%5E..Checkpoint_2.8", 179 | permanent: true, 180 | }, 181 | { 182 | source: "/checkpoint-2.9", 183 | destination: 184 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.9%5E..Checkpoint_2.9", 185 | permanent: true, 186 | }, 187 | { 188 | source: "/checkpoint-2.10", 189 | destination: 190 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.10/pages/demo.jsx", 191 | permanent: true, 192 | }, 193 | { 194 | source: "/checkpoint-2.11", 195 | destination: 196 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.11/pages/index.jsx", 197 | permanent: true, 198 | }, 199 | { 200 | source: "/checkpoint-2.12", 201 | destination: 202 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_2.12/components/TodoList.jsx", 203 | permanent: true, 204 | }, 205 | { 206 | source: "/checkpoint-2.13", 207 | destination: 208 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_2.12..Checkpoint_2.13", 209 | permanent: true, 210 | }, 211 | { 212 | source: "/checkpoint-3.0", 213 | destination: 214 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_3.0/components/TodoInput.jsx#L29-L54", 215 | permanent: true, 216 | }, 217 | { 218 | source: "/checkpoint-3.1", 219 | destination: 220 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_3.1%5E%5E%5E%5E..Checkpoint_3.1", 221 | permanent: true, 222 | }, 223 | { 224 | source: "/checkpoint-3.2", 225 | destination: 226 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_3.2%5E%5E..Checkpoint_3.2", 227 | permanent: true, 228 | }, 229 | { 230 | source: "/checkpoint-3.3", 231 | destination: 232 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_3.3/pages/index.jsx#L55", 233 | permanent: true, 234 | }, 235 | { 236 | source: "/checkpoint-3.4", 237 | destination: 238 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_3.4/components/TaskCountSettings.jsx#L16-L25", 239 | permanent: true, 240 | }, 241 | { 242 | source: "/checkpoint-3.5", 243 | destination: 244 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/compare/Checkpoint_3.4..Checkpoint_3.5", 245 | permanent: true, 246 | }, 247 | { 248 | source: "/checkpoint-3.6", 249 | destination: 250 | "https://github.com/PacktPublishing/Architecting-Enterprise-React-Applications-with-Hooks/blob/Checkpoint_3.6/components/TodoInput.jsx#L29-L56", 251 | permanent: true, 252 | }, 253 | ]; 254 | }, 255 | }; 256 | --------------------------------------------------------------------------------