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