├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.test.tsx
├── App.tsx
├── components
│ ├── ColorModeSwitcher.tsx
│ ├── TodoAdd.tsx
│ ├── TodoList.tsx
│ └── TopBar.tsx
├── index.tsx
├── react-app-env.d.ts
├── reportWebVitals.ts
├── serviceWorker.ts
├── setupTests.ts
├── store.ts
└── test-utils.tsx
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Base project for the Todo state manager series
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todo-base",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@chakra-ui/react": "^1.0.0",
7 | "@emotion/react": "^11.0.0",
8 | "@emotion/styled": "^11.0.0",
9 | "@testing-library/jest-dom": "^5.9.0",
10 | "@testing-library/react": "^10.2.1",
11 | "@testing-library/user-event": "^12.0.2",
12 | "@types/jest": "^25.0.0",
13 | "@types/node": "^12.0.0",
14 | "@types/react": "^16.9.0",
15 | "@types/react-dom": "^16.9.0",
16 | "framer-motion": ">=3.0.0",
17 | "react": "^17.0.1",
18 | "react-dom": "^17.0.1",
19 | "react-icons": "^3.0.0",
20 | "react-scripts": "4.0.2",
21 | "typescript": "^3.9.5",
22 | "web-vitals": "^0.2.2"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject"
29 | },
30 | "eslintConfig": {
31 | "extends": "react-app"
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jherr/todo-base/9ef31cc20fdb0dfc0ab4b7a1435254db4e8648ff/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Base Todo List
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jherr/todo-base/9ef31cc20fdb0dfc0ab4b7a1435254db4e8648ff/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jherr/todo-base/9ef31cc20fdb0dfc0ab4b7a1435254db4e8648ff/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { screen } from "@testing-library/react"
3 | import { render } from "./test-utils"
4 | import { App } from "./App"
5 |
6 | test("renders learn react link", () => {
7 | render()
8 | const linkElement = screen.getByText(/learn chakra/i)
9 | expect(linkElement).toBeInTheDocument()
10 | })
11 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { ChakraProvider, Box, theme } from "@chakra-ui/react";
3 | import TopBar from "./components/TopBar";
4 | import TodoList from "./components/TodoList";
5 | import TodoAdd from "./components/TodoAdd";
6 |
7 | export function App() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/ColorModeSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import {
3 | useColorMode,
4 | useColorModeValue,
5 | IconButton,
6 | IconButtonProps,
7 | } from "@chakra-ui/react"
8 | import { FaMoon, FaSun } from "react-icons/fa"
9 |
10 | type ColorModeSwitcherProps = Omit
11 |
12 | export const ColorModeSwitcher: React.FC = (props) => {
13 | const { toggleColorMode } = useColorMode()
14 | const text = useColorModeValue("dark", "light")
15 | const SwitchIcon = useColorModeValue(FaMoon, FaSun)
16 |
17 | return (
18 | }
26 | aria-label={`Switch to ${text} mode`}
27 | {...props}
28 | />
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/TodoAdd.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Button, Input, Grid } from "@chakra-ui/react";
3 |
4 | function TodoAdd() {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default TodoAdd;
14 |
--------------------------------------------------------------------------------
/src/components/TodoList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Button, Input, Flex, Checkbox, Heading } from "@chakra-ui/react";
3 |
4 | function TodoListItems() {
5 | return (
6 | <>
7 | {[].map((todo: { id: number; text: string }) => (
8 |
9 |
10 |
11 |
12 |
13 | ))}
14 | >
15 | );
16 | }
17 |
18 | function TodoList() {
19 | return (
20 | <>
21 | Todo List
22 |
23 | >
24 | );
25 | }
26 |
27 | export default TodoList;
28 |
--------------------------------------------------------------------------------
/src/components/TopBar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Button, Grid } from "@chakra-ui/react";
3 | import { ColorModeSwitcher } from "./ColorModeSwitcher";
4 |
5 | /*
6 | JSON source: https://raw.githubusercontent.com/jherr/todos-four-ways/master/data/todos.json
7 | */
8 |
9 | function TopBar() {
10 | return (
11 |
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | export default TopBar;
19 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { ColorModeScript } from "@chakra-ui/react"
2 | import * as React from "react"
3 | import ReactDOM from "react-dom"
4 | import { App } from "./App"
5 | import reportWebVitals from "./reportWebVitals"
6 | import * as serviceWorker from "./serviceWorker"
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 | ,
13 | document.getElementById("root"),
14 | )
15 |
16 | // If you want your app to work offline and load faster, you can change
17 | // unregister() to register() below. Note this comes with some pitfalls.
18 | // Learn more about service workers: https://cra.link/PWA
19 | serviceWorker.unregister()
20 |
21 | // If you want to start measuring performance in your app, pass a function
22 | // to log results (for example: reportWebVitals(console.log))
23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
24 | reportWebVitals()
25 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from "web-vitals"
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry)
7 | getFID(onPerfEntry)
8 | getFCP(onPerfEntry)
9 | getLCP(onPerfEntry)
10 | getTTFB(onPerfEntry)
11 | })
12 | }
13 | }
14 |
15 | export default reportWebVitals
16 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://cra.link/PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === "localhost" ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === "[::1]" ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
20 | ),
21 | )
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void
26 | }
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
32 | if (publicUrl.origin !== window.location.origin) {
33 | // Our service worker won't work if PUBLIC_URL is on a different origin
34 | // from what our page is served on. This might happen if a CDN is used to
35 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
36 | return
37 | }
38 |
39 | window.addEventListener("load", () => {
40 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
41 |
42 | if (isLocalhost) {
43 | // This is running on localhost. Let's check if a service worker still exists or not.
44 | checkValidServiceWorker(swUrl, config)
45 |
46 | // Add some additional logging to localhost, pointing developers to the
47 | // service worker/PWA documentation.
48 | navigator.serviceWorker.ready.then(() => {
49 | console.log(
50 | "This web app is being served cache-first by a service " +
51 | "worker. To learn more, visit https://cra.link/PWA",
52 | )
53 | })
54 | } else {
55 | // Is not localhost. Just register service worker
56 | registerValidSW(swUrl, config)
57 | }
58 | })
59 | }
60 | }
61 |
62 | function registerValidSW(swUrl: string, config?: Config) {
63 | navigator.serviceWorker
64 | .register(swUrl)
65 | .then((registration) => {
66 | registration.onupdatefound = () => {
67 | const installingWorker = registration.installing
68 | if (installingWorker == null) {
69 | return
70 | }
71 | installingWorker.onstatechange = () => {
72 | if (installingWorker.state === "installed") {
73 | if (navigator.serviceWorker.controller) {
74 | // At this point, the updated precached content has been fetched,
75 | // but the previous service worker will still serve the older
76 | // content until all client tabs are closed.
77 | console.log(
78 | "New content is available and will be used when all " +
79 | "tabs for this page are closed. See https://cra.link/PWA.",
80 | )
81 |
82 | // Execute callback
83 | if (config && config.onUpdate) {
84 | config.onUpdate(registration)
85 | }
86 | } else {
87 | // At this point, everything has been precached.
88 | // It is the perfect time to display a
89 | // "Content is cached for offline use." message.
90 | console.log("Content is cached for offline use.")
91 |
92 | // Execute callback
93 | if (config && config.onSuccess) {
94 | config.onSuccess(registration)
95 | }
96 | }
97 | }
98 | }
99 | }
100 | })
101 | .catch((error) => {
102 | console.error("Error during service worker registration:", error)
103 | })
104 | }
105 |
106 | function checkValidServiceWorker(swUrl: string, config?: Config) {
107 | // Check if the service worker can be found. If it can't reload the page.
108 | fetch(swUrl, {
109 | headers: { "Service-Worker": "script" },
110 | })
111 | .then((response) => {
112 | // Ensure service worker exists, and that we really are getting a JS file.
113 | const contentType = response.headers.get("content-type")
114 | if (
115 | response.status === 404 ||
116 | (contentType != null && contentType.indexOf("javascript") === -1)
117 | ) {
118 | // No service worker found. Probably a different app. Reload the page.
119 | navigator.serviceWorker.ready.then((registration) => {
120 | registration.unregister().then(() => {
121 | window.location.reload()
122 | })
123 | })
124 | } else {
125 | // Service worker found. Proceed as normal.
126 | registerValidSW(swUrl, config)
127 | }
128 | })
129 | .catch(() => {
130 | console.log(
131 | "No internet connection found. App is running in offline mode.",
132 | )
133 | })
134 | }
135 |
136 | export function unregister() {
137 | if ("serviceWorker" in navigator) {
138 | navigator.serviceWorker.ready
139 | .then((registration) => {
140 | registration.unregister()
141 | })
142 | .catch((error) => {
143 | console.error(error.message)
144 | })
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom"
6 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | // Standard interface and functions
2 | export interface Todo {
3 | id: number;
4 | text: string;
5 | done: boolean;
6 | }
7 |
8 | export const updateTodo = (todos: Todo[], id: number, text: string): Todo[] =>
9 | todos.map((todo) => ({
10 | ...todo,
11 | text: todo.id === id ? text : todo.text,
12 | }));
13 |
14 | export const toggleTodo = (todos: Todo[], id: number): Todo[] =>
15 | todos.map((todo) => ({
16 | ...todo,
17 | done: todo.id === id ? !todo.done : todo.done,
18 | }));
19 |
20 | export const removeTodo = (todos: Todo[], id: number): Todo[] =>
21 | todos.filter((todo) => todo.id !== id);
22 |
23 | export const addTodo = (todos: Todo[], text: string): Todo[] => [
24 | ...todos,
25 | {
26 | id: Math.max(0, Math.max(...todos.map(({ id }) => id))) + 1,
27 | text,
28 | done: false,
29 | },
30 | ];
31 |
--------------------------------------------------------------------------------
/src/test-utils.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { render, RenderOptions } from "@testing-library/react"
3 | import { ChakraProvider, theme } from "@chakra-ui/react"
4 |
5 | const AllProviders = ({ children }: { children?: React.ReactNode }) => (
6 | {children}
7 | )
8 |
9 | const customRender = (ui: React.ReactElement, options?: RenderOptions) =>
10 | render(ui, { wrapper: AllProviders, ...options })
11 |
12 | export { customRender as render }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------