├── .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 |
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 |
--------------------------------------------------------------------------------