├── .all-contributorsrc ├── .env ├── .eslintcache ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo.png ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── components │ ├── Actions │ │ ├── ActionsMenu.tsx │ │ ├── DeleteConfirm.tsx │ │ ├── EditConfirm.tsx │ │ └── MoreMenu.tsx │ ├── CustomLink.tsx │ ├── PersistentDrawerLeft.tsx │ └── Todos │ │ ├── AddTodo.tsx │ │ ├── Todo.tsx │ │ └── Todos.tsx ├── context │ ├── DeleteConfirmContext.tsx │ ├── MainContext.tsx │ ├── SmallTextContext.tsx │ └── ThemeContext.tsx ├── hooks │ └── useChangeMenuIcon.tsx ├── index.tsx ├── pages │ ├── About.tsx │ └── Settings.tsx ├── react-app-env.d.ts ├── service-worker.ts ├── serviceWorkerRegistration.ts ├── styles.css └── types.d.ts ├── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "Steffan153", 10 | "name": "Steffan153", 11 | "avatar_url": "https://avatars0.githubusercontent.com/u/40404519?v=4", 12 | "profile": "https://github.com/Steffan153", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "worachatsun", 19 | "name": "Worachat Arunothaikrit", 20 | "avatar_url": "https://avatars0.githubusercontent.com/u/9085914?v=4", 21 | "profile": "https://github.com/worachatsun", 22 | "contributions": [ 23 | "code" 24 | ] 25 | }, 26 | { 27 | "login": "m11dedhia", 28 | "name": "Megh Dedhia", 29 | "avatar_url": "https://avatars3.githubusercontent.com/u/13602231?v=4", 30 | "profile": "https://github.com/m11dedhia", 31 | "contributions": [ 32 | "code" 33 | ] 34 | }, 35 | { 36 | "login": "lucas-jg", 37 | "name": "Lucas", 38 | "avatar_url": "https://avatars2.githubusercontent.com/u/31200025?v=4", 39 | "profile": "https://github.com/lucas-jg", 40 | "contributions": [ 41 | "code" 42 | ] 43 | }, 44 | { 45 | "login": "ykchan052", 46 | "name": "Nicholas Chan", 47 | "avatar_url": "https://avatars3.githubusercontent.com/u/11728676?v=4", 48 | "profile": "https://github.com/ykchan052", 49 | "contributions": [ 50 | "code" 51 | ] 52 | }, 53 | { 54 | "login": "tusharkashyap63", 55 | "name": "Tushar Kashyap", 56 | "avatar_url": "https://avatars3.githubusercontent.com/u/65089058?v=4", 57 | "profile": "https://dev.to/tusharkashyap63", 58 | "contributions": [ 59 | "code" 60 | ] 61 | }, 62 | { 63 | "login": "DharmarajX24", 64 | "name": "Dharmaraj", 65 | "avatar_url": "https://avatars2.githubusercontent.com/u/63334359?v=4", 66 | "profile": "https://github.com/DharmarajX24", 67 | "contributions": [ 68 | "code" 69 | ] 70 | }, 71 | { 72 | "login": "Chensokheng", 73 | "name": "Chensokheng", 74 | "avatar_url": "https://avatars2.githubusercontent.com/u/52232579?v=4", 75 | "profile": "https://github.com/Chensokheng", 76 | "contributions": [ 77 | "code" 78 | ] 79 | } 80 | ], 81 | "contributorsPerLine": 7, 82 | "projectName": "max-todos", 83 | "projectOwner": "max-programming", 84 | "repoType": "github", 85 | "repoHost": "https://github.com", 86 | "skipCi": true 87 | } 88 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | EXTEND_ESLINT=true -------------------------------------------------------------------------------- /.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/mnt/d/Contribs/max-todos/src/App.tsx":"1","/mnt/d/Contribs/max-todos/src/components/Actions/ActionsMenu.tsx":"2","/mnt/d/Contribs/max-todos/src/components/Actions/DeleteConfirm.tsx":"3","/mnt/d/Contribs/max-todos/src/components/Actions/EditConfirm.tsx":"4","/mnt/d/Contribs/max-todos/src/components/Actions/MoreMenu.tsx":"5","/mnt/d/Contribs/max-todos/src/components/CustomLink.tsx":"6","/mnt/d/Contribs/max-todos/src/components/PersistentDrawerLeft.tsx":"7","/mnt/d/Contribs/max-todos/src/components/Todos/AddTodo.tsx":"8","/mnt/d/Contribs/max-todos/src/components/Todos/Todo.tsx":"9","/mnt/d/Contribs/max-todos/src/components/Todos/Todos.tsx":"10","/mnt/d/Contribs/max-todos/src/context/DeleteConfirmContext.tsx":"11","/mnt/d/Contribs/max-todos/src/context/MainContext.tsx":"12","/mnt/d/Contribs/max-todos/src/context/SmallTextContext.tsx":"13","/mnt/d/Contribs/max-todos/src/context/ThemeContext.tsx":"14","/mnt/d/Contribs/max-todos/src/hooks/useChangeMenuIcon.tsx":"15","/mnt/d/Contribs/max-todos/src/index.tsx":"16","/mnt/d/Contribs/max-todos/src/pages/About.tsx":"17","/mnt/d/Contribs/max-todos/src/pages/Settings.tsx":"18","/mnt/d/Contribs/max-todos/src/react-app-env.d.ts":"19","/mnt/d/Contribs/max-todos/src/service-worker.ts":"20","/mnt/d/Contribs/max-todos/src/serviceWorkerRegistration.ts":"21","/mnt/d/Contribs/max-todos/src/types.d.ts":"22"},{"size":771,"mtime":1608956416692,"results":"23","hashOfConfig":"24"},{"size":3470,"mtime":1608966501592,"results":"25","hashOfConfig":"24"},{"size":1824,"mtime":1608963532361,"results":"26","hashOfConfig":"24"},{"size":1375,"mtime":1608963630120,"results":"27","hashOfConfig":"24"},{"size":2656,"mtime":1608957588884,"results":"28","hashOfConfig":"24"},{"size":447,"mtime":1608963838120,"results":"29","hashOfConfig":"24"},{"size":7748,"mtime":1608966043418,"results":"30","hashOfConfig":"24"},{"size":1810,"mtime":1608958200315,"results":"31","hashOfConfig":"24"},{"size":4719,"mtime":1608966535080,"results":"32","hashOfConfig":"24"},{"size":2502,"mtime":1608958575807,"results":"33","hashOfConfig":"24"},{"size":934,"mtime":1608965053370,"results":"34","hashOfConfig":"24"},{"size":2805,"mtime":1608966583799,"results":"35","hashOfConfig":"24"},{"size":808,"mtime":1608965055870,"results":"36","hashOfConfig":"24"},{"size":1212,"mtime":1608965057741,"results":"37","hashOfConfig":"24"},{"size":320,"mtime":1608965841849,"results":"38","hashOfConfig":"24"},{"size":907,"mtime":1608879309918,"results":"39","hashOfConfig":"24"},{"size":2130,"mtime":1608966267876,"results":"40","hashOfConfig":"24"},{"size":1264,"mtime":1608961096814,"results":"41","hashOfConfig":"24"},{"size":40,"mtime":1608954726052,"results":"42","hashOfConfig":"24"},{"size":2972,"mtime":1608879369277,"results":"43","hashOfConfig":"24"},{"size":5284,"mtime":1608879390082,"results":"44","hashOfConfig":"24"},{"size":95,"mtime":1608966410980,"results":"45","hashOfConfig":"24"},{"filePath":"46","messages":"47","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"ty89r0",{"filePath":"48","messages":"49","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"50","messages":"51","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"52","messages":"53","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"54","messages":"55","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"56","messages":"57","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"58","messages":"59","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"60","messages":"61","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"62","messages":"63","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"64","messages":"65","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"66","messages":"67","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"68","messages":"69","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"70","messages":"71","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"72","messages":"73","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"74","messages":"75","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"76","messages":"77","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"78","messages":"79","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"80","messages":"81","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"82","messages":"83","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"84","messages":"85","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"86","messages":"87","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"88","messages":"89","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/mnt/d/Contribs/max-todos/src/App.tsx",[],"/mnt/d/Contribs/max-todos/src/components/Actions/ActionsMenu.tsx",[],"/mnt/d/Contribs/max-todos/src/components/Actions/DeleteConfirm.tsx",[],"/mnt/d/Contribs/max-todos/src/components/Actions/EditConfirm.tsx",[],"/mnt/d/Contribs/max-todos/src/components/Actions/MoreMenu.tsx",[],"/mnt/d/Contribs/max-todos/src/components/CustomLink.tsx",[],"/mnt/d/Contribs/max-todos/src/components/PersistentDrawerLeft.tsx",[],"/mnt/d/Contribs/max-todos/src/components/Todos/AddTodo.tsx",[],"/mnt/d/Contribs/max-todos/src/components/Todos/Todo.tsx",[],"/mnt/d/Contribs/max-todos/src/components/Todos/Todos.tsx",[],"/mnt/d/Contribs/max-todos/src/context/DeleteConfirmContext.tsx",[],"/mnt/d/Contribs/max-todos/src/context/MainContext.tsx",[],"/mnt/d/Contribs/max-todos/src/context/SmallTextContext.tsx",[],"/mnt/d/Contribs/max-todos/src/context/ThemeContext.tsx",[],"/mnt/d/Contribs/max-todos/src/hooks/useChangeMenuIcon.tsx",[],"/mnt/d/Contribs/max-todos/src/index.tsx",[],"/mnt/d/Contribs/max-todos/src/pages/About.tsx",[],"/mnt/d/Contribs/max-todos/src/pages/Settings.tsx",[],"/mnt/d/Contribs/max-todos/src/react-app-env.d.ts",[],"/mnt/d/Contribs/max-todos/src/service-worker.ts",[],"/mnt/d/Contribs/max-todos/src/serviceWorkerRegistration.ts",[],"/mnt/d/Contribs/max-todos/src/types.d.ts",[]] -------------------------------------------------------------------------------- /.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 | /backup -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Max Programming 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | Max Todos 📝 4 |

5 | 6 |

7 | 🖊️ Todo App built using React and Material UI 8 |

9 |

10 | 11 | Gitmoji 12 | 13 | 14 | 15 | [![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) 16 | 17 |

18 | 19 | ## This is a super simple Todo App built using React.js and styled using Material UI. 20 | 21 | It uses [TypeScript](https://www.typescriptlang.org/), [Context API](https://reactjs.org/docs/context.html) to manage state, [Material UI](https://material-ui.com/) for design, [Wouter](https://github.com/molefrog/wouter) for routing, [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to store todos and Dark Mode support with [Dark Reader](https://darkreader.org/). 22 | 23 | ### You can view it here: https://maxtodos.netlify.app/ 24 | 25 | ## 😃 You can: 26 | 27 | - ➕ Add todo 28 | - 🖊️ Edit todo 29 | - 🗑️ Delete todo 30 | - ↕️ Reorder the todos by dragging them 31 | - 🕶️ Enable dark mode 32 | 33 | ## 🧠 Features in mind 34 | 35 | - [ ] Use [Material UI's Dark Mode](https://material-ui.com/customization/palette/#user-preference) 36 | - [ ] Set the dark mode as per user's system theme. More at [#43](https://github.com/max-programming/max-todos/issues/43) 37 | - [ ] Improve the app using Lighthouse suggestions. More at [#22](https://github.com/max-programming/max-todos/issues/22) 38 | 39 | ## 🏗️ To build it 40 | 41 | 1. Clone this repo by running `git clone https://github.com/max-programming/max-todos.git`. 42 | 2. `cd` into the `max-todos` folder and run `npm i` OR `yarn`. 43 | 3. Run `npm start` OR `yarn start` to start the development server. 44 | 45 | ## Contributors ✨ 46 | 47 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |

Steffan153

💻

Worachat Arunothaikrit

💻

Megh Dedhia

💻

Lucas

💻

Nicholas Chan

💻

Tushar Kashyap

💻

Dharmaraj

💻

Chensokheng

💻
66 | 67 | 68 | 69 | 70 | 71 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 72 | 73 | ❤️ Feel free to create issues and contributions for features or bugs to this project. 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "max-todos", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.2", 7 | "@material-ui/icons": "^4.11.2", 8 | "@material-ui/lab": "^4.0.0-alpha.57", 9 | "@types/node": "^14.14.16", 10 | "@types/react": "^17.0.0", 11 | "@types/react-dom": "^17.0.0", 12 | "darkreader": "^4.9.26", 13 | "react": "^17.0.1", 14 | "react-beautiful-dnd": "^13.0.0", 15 | "react-dom": "^17.0.1", 16 | "react-flip-move": "^3.0.4", 17 | "react-scripts": "^4.0.1", 18 | "typescript": "^4.1.3", 19 | "uuid": "^7.0.3", 20 | "wouter": "^2.7.0" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "@types/material-ui": "^0.21.8", 44 | "@types/react-beautiful-dnd": "^13.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-programming/max-todos/74b591078e5acee4968fc349dc959751bdc27661/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | Max Todos 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-programming/max-todos/74b591078e5acee4968fc349dc959751bdc27661/public/logo.png -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-programming/max-todos/74b591078e5acee4968fc349dc959751bdc27661/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-programming/max-todos/74b591078e5acee4968fc349dc959751bdc27661/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "A Powerful To-Do App", 3 | "name": "Max To-Dos", 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": "#3F51B5", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { Route } from "wouter"; 3 | import AddTodo from "./components/Todos/AddTodo"; 4 | import PersistentDrawerLeft from "./components/PersistentDrawerLeft"; 5 | import Todos from "./components/Todos/Todos"; 6 | import { MainContext } from "./context/MainContext"; 7 | import About from "./pages/About"; 8 | import Settings from "./pages/Settings"; 9 | 10 | function App() { 11 | const { addTodo } = useContext(MainContext)!; 12 | 13 | return ( 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | ); 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /src/components/Actions/ActionsMenu.tsx: -------------------------------------------------------------------------------- 1 | import { TodoType } from "../../types"; 2 | import { Typography, IconButton, Menu, MenuItem } from "@material-ui/core"; 3 | import { 4 | DeleteTwoTone as DeleteIcon, 5 | EditTwoTone as EditIcon, 6 | StarTwoTone as StarIconOutlined, 7 | Star as StarIcon, 8 | SvgIconComponent, 9 | } from "@material-ui/icons"; 10 | import useChangeMenuIcon from "../../hooks/useChangeMenuIcon"; 11 | import React, { useState } from "react"; 12 | 13 | const ITEM_HEIGHT = 48; 14 | 15 | interface Option { 16 | name: string; 17 | customColor?: string | undefined; 18 | iconColor?: 19 | | "error" 20 | | "action" 21 | | "inherit" 22 | | "disabled" 23 | | "primary" 24 | | "secondary" 25 | | undefined; 26 | textColor?: 27 | | "inherit" 28 | | "initial" 29 | | "error" 30 | | "primary" 31 | | "secondary" 32 | | "textPrimary" 33 | | "textSecondary" 34 | | undefined; 35 | icon: SvgIconComponent; 36 | method: (e?: React.MouseEvent) => void; 37 | } 38 | 39 | interface Props { 40 | deleteTodo: (e: any) => void; 41 | setEditOpen: React.Dispatch>; 42 | markStar: (id: string) => void; 43 | todo: TodoType; 44 | } 45 | 46 | enum OptionName { 47 | STAR = "Star", 48 | UNSTAR = "Unstar", 49 | EDIT = "Edit", 50 | DELETE = "Delete", 51 | } 52 | 53 | export default function ActionsMenu({ 54 | deleteTodo, 55 | setEditOpen, 56 | markStar, 57 | todo, 58 | }: Props) { 59 | const [anchorEl, setAnchorEl] = useState< 60 | (EventTarget & HTMLButtonElement) | null 61 | >(null); 62 | const open = Boolean(anchorEl); 63 | const MenuIcon = useChangeMenuIcon(); 64 | 65 | const options: Option[] = [ 66 | { 67 | name: todo.starred ? OptionName.UNSTAR : OptionName.STAR, 68 | customColor: todo.starred ? "#CCA43A" : "#000", 69 | icon: todo.starred ? StarIcon : StarIconOutlined, 70 | method: () => { 71 | markStar(todo.id); 72 | setAnchorEl(null); 73 | }, 74 | }, 75 | { 76 | name: OptionName.EDIT, 77 | iconColor: "primary", 78 | textColor: "primary", 79 | icon: EditIcon, 80 | method: () => { 81 | setEditOpen(true); 82 | setAnchorEl(null); 83 | }, 84 | }, 85 | { 86 | name: OptionName.DELETE, 87 | iconColor: "error", 88 | textColor: "error", 89 | icon: DeleteIcon, 90 | method: (e) => { 91 | deleteTodo(e); 92 | setAnchorEl(null); 93 | }, 94 | }, 95 | ]; 96 | const handleClick = (e: React.MouseEvent) => { 97 | setAnchorEl(e.currentTarget); 98 | }; 99 | 100 | const handleEvent = (option: OptionName, e: any) => { 101 | if (option === "Star") markStar(todo.id); 102 | else if (option === "Edit") setEditOpen(true); 103 | else if (option === "Delete") deleteTodo(e); 104 | setAnchorEl(null); 105 | }; 106 | 107 | return ( 108 |
109 | 116 | 117 | 118 | 131 | {options.map((option) => ( 132 | 133 | 137 |   138 | 142 | {option.name} 143 | 144 | 145 | ))} 146 | 147 |
148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /src/components/Actions/DeleteConfirm.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogActions, 4 | DialogContent, 5 | DialogContentText, 6 | DialogTitle, 7 | Button, 8 | Divider, 9 | useMediaQuery, 10 | } from "@material-ui/core"; 11 | 12 | interface Props { 13 | open: boolean; 14 | close: () => void; 15 | yes: () => void; 16 | } 17 | 18 | export const DeleteConfirm = ({ open, close, yes }: Props) => { 19 | const matches = useMediaQuery("(max-width: 768px)"); 20 | return ( 21 | 22 | DELETE ITEM? 23 | 24 | 25 | Are you sure you want to delete this item? 26 | 27 |
28 | 29 |
30 | 31 | PROTIP: 32 |
33 | You can hold down shift when clicking the delete button to 34 | bypass this confirmation entirely 35 |
36 |
37 |
38 | 39 | 42 | 45 | 46 |
47 | ); 48 | }; 49 | 50 | export const DeleteAllConfirm = ({ open, close, yes }: Props) => { 51 | return ( 52 | 53 | DELETE ALL ITEMS? 54 | 55 | 56 | Are you sure you want to delete all items? 57 | 58 | 59 | 60 | 63 | 66 | 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/Actions/EditConfirm.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogActions, 6 | DialogContent, 7 | DialogContentText, 8 | TextField, 9 | Button, 10 | } from "@material-ui/core"; 11 | 12 | interface Props { 13 | yes: (val: string) => void; 14 | open: boolean; 15 | close: () => void; 16 | value: string; 17 | } 18 | 19 | const EditConfirm = ({ open, close, value, yes }: Props) => { 20 | const [newValue, setNewValue] = useState(value); 21 | const onClose = () => { 22 | setNewValue(value); 23 | close(); 24 | }; 25 | return ( 26 | 27 | EDIT ITEM 28 | 29 | 30 | Please provide the new name for this item. 31 | 32 | setNewValue(e.target.value)} 41 | /> 42 | 43 | 44 | 47 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default EditConfirm; 60 | -------------------------------------------------------------------------------- /src/components/Actions/MoreMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from "react"; 2 | import { IconButton, Menu, MenuItem, Typography } from "@material-ui/core"; 3 | import { 4 | DeleteSweepTwoTone as DeleteSweepIcon, 5 | SvgIconComponent, 6 | } from "@material-ui/icons"; 7 | import useChangeMenuIcon from "../../hooks/useChangeMenuIcon"; 8 | import { MainContext } from "../../context/MainContext"; 9 | import { DeleteAllConfirm } from "./DeleteConfirm"; 10 | 11 | const MoreMenu = () => { 12 | const [anchorEl, setAnchorEl] = useState< 13 | (EventTarget & HTMLButtonElement) | null 14 | >(null); 15 | const [deleteOpen, setDeleteOpen] = useState(false); 16 | const open = Boolean(anchorEl); 17 | const MenuIcon = useChangeMenuIcon(); 18 | const { todos, deleteAll } = useContext(MainContext)!; 19 | 20 | const handleClick = (e: React.MouseEvent) => 21 | setAnchorEl(e.currentTarget); 22 | const handleClose = () => setAnchorEl(null); 23 | 24 | interface Option { 25 | name: string; 26 | iconColor: 27 | | "error" 28 | | "action" 29 | | "inherit" 30 | | "disabled" 31 | | "primary" 32 | | "secondary" 33 | | undefined; 34 | textColor: 35 | | "error" 36 | | "inherit" 37 | | "primary" 38 | | "secondary" 39 | | "initial" 40 | | "textPrimary" 41 | | "textSecondary" 42 | | undefined; 43 | disabled: boolean; 44 | icon: SvgIconComponent; 45 | method: () => void; 46 | } 47 | 48 | const options: Option[] = [ 49 | { 50 | name: "Delete All", 51 | iconColor: "error", 52 | textColor: "error", 53 | disabled: todos.length === 0, 54 | icon: DeleteSweepIcon, 55 | method: () => { 56 | handleClose(); 57 | setDeleteOpen(true); 58 | }, 59 | }, 60 | ]; 61 | 62 | return ( 63 |
64 | 71 | 72 | 73 | 85 | {options.map((option) => ( 86 | 91 |   92 | {option.name} 93 | 94 | ))} 95 | 96 | { 98 | setDeleteOpen(false); 99 | setTimeout(() => deleteAll(), 200); 100 | }} 101 | open={deleteOpen} 102 | close={() => setDeleteOpen(false)} 103 | /> 104 |
105 | ); 106 | }; 107 | 108 | export default MoreMenu; 109 | -------------------------------------------------------------------------------- /src/components/CustomLink.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useRoute, Link } from "wouter"; 3 | 4 | interface Props { 5 | href: string; 6 | onClick?: () => void; 7 | children: React.ReactNode; 8 | } 9 | 10 | const CustomLink = (props: Props) => { 11 | const [isActive] = useRoute(props.href); 12 | const activeObject = { 13 | color: "#3f51b5", 14 | }; 15 | return ( 16 | 17 | {props.children} 18 | 19 | ); 20 | }; 21 | 22 | export default CustomLink; 23 | -------------------------------------------------------------------------------- /src/components/PersistentDrawerLeft.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppBar, 3 | CssBaseline, 4 | Divider, 5 | Drawer, 6 | IconButton, 7 | List, 8 | ListItem, 9 | ListItemIcon, 10 | ListItemText, 11 | makeStyles, 12 | useTheme, 13 | Toolbar, 14 | Typography, 15 | Slide, 16 | Button, 17 | useScrollTrigger, 18 | useMediaQuery, 19 | } from "@material-ui/core"; 20 | import { 21 | ChevronLeft as ChevronLeftIcon, 22 | ChevronRight as ChevronRightIcon, 23 | NotesOutlined as TodoIcon, 24 | InfoOutlined as AboutIconOutlined, 25 | Info as AboutIcon, 26 | Menu as MenuIcon, 27 | ArrowBack as BackIcon, 28 | SettingsOutlined as SettingsIconOutlined, 29 | Settings as SettingsIcon, 30 | } from "@material-ui/icons"; 31 | import { Link, useLocation } from "wouter"; 32 | import clsx from "clsx"; 33 | import { useState } from "react"; 34 | import CustomLink from "./CustomLink"; 35 | import MoreMenu from "./Actions/MoreMenu"; 36 | 37 | const drawerWidth = 240; 38 | 39 | const useStyles = makeStyles((theme) => ({ 40 | root: { 41 | display: "flex", 42 | }, 43 | appBar: { 44 | transition: theme.transitions.create(["margin", "width"], { 45 | easing: theme.transitions.easing.sharp, 46 | duration: theme.transitions.duration.leavingScreen, 47 | }), 48 | }, 49 | appBarShift: { 50 | width: `calc(100% - ${drawerWidth}px)`, 51 | marginLeft: drawerWidth, 52 | transition: theme.transitions.create(["margin", "width"], { 53 | easing: theme.transitions.easing.easeOut, 54 | duration: theme.transitions.duration.enteringScreen, 55 | }), 56 | }, 57 | menuButton: { 58 | marginRight: theme.spacing(2), 59 | }, 60 | hide: { 61 | display: "none", 62 | }, 63 | drawer: { 64 | width: drawerWidth, 65 | flexShrink: 0, 66 | }, 67 | drawerPaper: { 68 | width: drawerWidth, 69 | }, 70 | drawerHeader: { 71 | display: "flex", 72 | alignItems: "center", 73 | padding: theme.spacing(0, 1), 74 | // necessary for content to be below app bar 75 | ...theme.mixins.toolbar, 76 | justifyContent: "flex-end", 77 | }, 78 | content: { 79 | flexGrow: 1, 80 | padding: theme.spacing(1), 81 | transition: theme.transitions.create("margin", { 82 | easing: theme.transitions.easing.sharp, 83 | duration: theme.transitions.duration.leavingScreen, 84 | }), 85 | marginLeft: -drawerWidth, 86 | }, 87 | contentShift: { 88 | transition: theme.transitions.create("margin", { 89 | easing: theme.transitions.easing.easeOut, 90 | duration: theme.transitions.duration.enteringScreen, 91 | }), 92 | marginLeft: 0, 93 | }, 94 | })); 95 | 96 | interface Props { 97 | window?: () => Window; 98 | children: React.ReactElement; 99 | } 100 | 101 | function HideOnScroll(props: Props) { 102 | const { children, window } = props; 103 | // Note that you normally won't need to set the window ref as useScrollTrigger 104 | // will default to window. 105 | // This is only being set here because the demo is in an iframe. 106 | const trigger = useScrollTrigger({ target: window ? window() : undefined }); 107 | 108 | return ( 109 | 110 | {children} 111 | 112 | ); 113 | } 114 | 115 | export default function PersistentDrawerLeft(props: any) { 116 | const classes = useStyles(); 117 | const theme = useTheme(); 118 | const [open, setOpen] = useState(false); 119 | const [location] = useLocation(); 120 | const matches = useMediaQuery("(max-width: 768px)"); 121 | const handleDrawerOpen = () => { 122 | setOpen(true); 123 | }; 124 | 125 | const handleDrawerClose = () => { 126 | setOpen(false); 127 | }; 128 | 129 | return ( 130 |
131 | 132 | 133 | 139 | 140 | {matches ? ( 141 | location === "/" ? ( 142 | 150 | 151 | 152 | ) : ( 153 | 154 | 161 | 162 | 163 | 164 | ) 165 | ) : ( 166 | "" 167 | )} 168 | {!open && ( 169 | <> 170 | {matches ? ( 171 | 176 | {location === "/" 177 | ? "MAX TODOS" 178 | : location.toUpperCase().replace("/", "")} 179 | 180 | ) : ( 181 | 182 | 187 | MAX TODOS 188 | 189 | 190 | )} 191 | 192 | {!matches && ( 193 | <> 194 | {["Settings", "About"].map((name, i) => ( 195 | 196 | 214 | 215 | ))} 216 | 217 | )} 218 | {location === "/" && } 219 | 220 | )} 221 | 222 | 223 | 224 | 233 |
234 | 235 | {theme.direction === "ltr" ? ( 236 | 237 | ) : ( 238 | 239 | )} 240 | 241 |
242 | 243 | 244 | {["Todos", "Settings", "About"].map((text, index) => ( 245 | 250 | 251 | 252 | {index === 0 ? ( 253 | 254 | ) : index === 1 ? ( 255 | 256 | ) : ( 257 | 258 | )} 259 | 260 | 261 | 262 | 263 | ))} 264 | 265 |
266 |
271 |
272 |
273 |
274 | ); 275 | } 276 | -------------------------------------------------------------------------------- /src/components/Todos/AddTodo.tsx: -------------------------------------------------------------------------------- 1 | import { useState, FC, ChangeEvent } from "react"; 2 | import { 3 | FormControl, 4 | Container, 5 | Button, 6 | TextField, 7 | Snackbar, 8 | } from "@material-ui/core"; 9 | import { Alert } from "@material-ui/lab"; 10 | import { Add } from "@material-ui/icons"; 11 | 12 | const AddTodo: FC<{ addTodo: (text: string) => void }> = ({ addTodo }) => { 13 | const [text, setText] = useState(""); 14 | const [open, setOpen] = useState(false); 15 | const handleChange = ( 16 | e: ChangeEvent 17 | ) => setText(e.target.value); 18 | const createTodo = (e: React.FormEvent) => { 19 | e.preventDefault(); 20 | addTodo(text); 21 | setText(""); 22 | if (text.trim()) setOpen(true); 23 | }; 24 | 25 | return ( 26 |
27 | 28 |
29 | 30 | 37 | 46 | 47 |
48 |
49 | setOpen(false)} 53 | anchorOrigin={{ vertical: "bottom", horizontal: "right" }} 54 | > 55 | } 57 | elevation={6} 58 | variant="filled" 59 | onClose={() => setOpen(false)} 60 | severity="success" 61 | > 62 | Successfully added item! 63 | 64 | 65 |
66 | ); 67 | }; 68 | 69 | export default AddTodo; 70 | -------------------------------------------------------------------------------- /src/components/Todos/Todo.tsx: -------------------------------------------------------------------------------- 1 | import { TodoType } from "../../types"; 2 | import React, { useState, useContext, forwardRef } from "react"; 3 | import { DeleteConfirm } from "../Actions/DeleteConfirm"; 4 | import EditConfirm from "../Actions/EditConfirm"; 5 | import { 6 | Card, 7 | CardContent, 8 | Typography, 9 | Container, 10 | useMediaQuery, 11 | Checkbox, 12 | Grid, 13 | } from "@material-ui/core"; 14 | import { Draggable } from "react-beautiful-dnd"; 15 | import { DeleteConfirmContext } from "../../context/DeleteConfirmContext"; 16 | import ActionsMenu from "../Actions/ActionsMenu"; 17 | import { SmallTextContext } from "../../context/SmallTextContext"; 18 | import { ThemeContext } from "../../context/ThemeContext"; 19 | import { MainContext } from "../../context/MainContext"; 20 | 21 | interface Props { 22 | todo: TodoType; 23 | index: number; 24 | onDelete: () => void; 25 | onEdit: () => void; 26 | } 27 | 28 | const Todo = forwardRef( 29 | ({ todo, index, onDelete, onEdit }: Props, ref: any) => { 30 | const { markComplete, delTodo, editTodo, markStar } = useContext( 31 | MainContext 32 | )!; 33 | const matches = useMediaQuery("(max-width: 768px)"); 34 | const [deleteOpen, setDeleteOpen] = useState(false); 35 | const [editOpen, setEditOpen] = useState(false); 36 | const { isDeleteConfirmation } = useContext(DeleteConfirmContext)!; 37 | const { isSmallText } = useContext(SmallTextContext)!; 38 | const { isDark } = useContext(ThemeContext)!; 39 | let checkedStyle = { textDecoration: "none" }; 40 | if (todo.completed) checkedStyle.textDecoration = "line-through"; 41 | else checkedStyle.textDecoration = "none"; 42 | 43 | const styles: any = { 44 | card: { 45 | marginTop: matches ? 20 : 35, 46 | background: "lightgray", 47 | }, 48 | icon: { 49 | float: "right", 50 | paddingTop: "10px", 51 | }, 52 | text: { 53 | wordBreak: "break-word", 54 | display: "-webkit-box", 55 | WebkitLineClamp: 2, 56 | WebkitBoxOrient: "vertical", 57 | overflow: "hidden", 58 | fontWeight: todo.starred ? 600 : "normal", 59 | fontSize: matches ? "17px" : isSmallText ? "17px" : "24px", 60 | color: "", 61 | }, 62 | }; 63 | 64 | if (todo.starred) { 65 | styles.text.color = isDark ? "#ffe066" : "#3f51b5"; 66 | } 67 | 68 | const deleteTodo = (e: any) => { 69 | if (e.shiftKey || isDeleteConfirmation) { 70 | delTodo(todo.id); 71 | onDelete(); 72 | } else setDeleteOpen(true); 73 | }; 74 | return ( 75 | 76 | 77 | {(p) => ( 78 | 90 | 91 | 97 | 98 | 99 | markComplete(todo.id)} 104 | centerRipple={false} 105 | /> 106 | 107 | 108 |
{todo.title}
109 |
110 | 111 | deleteTodo(e)} 113 | setEditOpen={setEditOpen} 114 | todo={todo} 115 | markStar={markStar} 116 | /> 117 | 118 |
119 |
120 |
121 |
122 | )} 123 |
124 | { 126 | setDeleteOpen(false); 127 | setTimeout(() => { 128 | delTodo(todo.id); 129 | onDelete(); 130 | }, 200); 131 | }} 132 | open={deleteOpen} 133 | close={() => setDeleteOpen(false)} 134 | /> 135 | { 137 | setEditOpen(false); 138 | setTimeout(() => { 139 | editTodo(todo.id, val); 140 | onEdit(); 141 | }, 200); 142 | }} 143 | open={editOpen} 144 | close={() => setEditOpen(false)} 145 | value={todo.title} 146 | /> 147 |
148 | ); 149 | } 150 | ); 151 | 152 | export default Todo; 153 | -------------------------------------------------------------------------------- /src/components/Todos/Todos.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useState } from "react"; 2 | import { MainContext } from "../../context/MainContext"; 3 | import { Droppable, DragDropContext, DropResult } from "react-beautiful-dnd"; 4 | import Todo from "./Todo"; 5 | import { Snackbar } from "@material-ui/core"; 6 | import { Alert } from "@material-ui/lab"; 7 | import FlipMove from "react-flip-move"; 8 | 9 | const Todos = () => { 10 | const { todos, moveTodo } = useContext(MainContext)!; 11 | const [deleteSnackOpen, setDeleteSnackOpen] = useState(false); 12 | const [editSnackOpen, setEditSnackOpen] = useState(false); 13 | const [dragging, setDragging] = useState(false); 14 | const onDragEnd = (x: DropResult) => { 15 | if (!x.destination) return console.log(x); 16 | moveTodo(x.source.index, x.destination.index); 17 | setTimeout(() => setDragging(false), 200); 18 | }; 19 | return ( 20 | <> 21 | setDragging(true)} 23 | onDragEnd={onDragEnd} 24 | > 25 | 26 | {(p) => ( 27 |
28 | 29 | {todos.map((todo, i) => { 30 | return ( 31 | setDeleteSnackOpen(true)} 35 | index={i} 36 | onEdit={() => setEditSnackOpen(true)} 37 | /> 38 | ); 39 | })} 40 | 41 | {p.placeholder} 42 |
43 | )} 44 |
45 |
46 | 47 | setDeleteSnackOpen(false)} 51 | anchorOrigin={{ vertical: "bottom", horizontal: "right" }} 52 | > 53 | setDeleteSnackOpen(false)} 57 | severity="success" 58 | > 59 | Successfully deleted item! 60 | 61 | 62 | setEditSnackOpen(false)} 66 | anchorOrigin={{ vertical: "bottom", horizontal: "right" }} 67 | > 68 | setEditSnackOpen(false)} 72 | severity="success" 73 | > 74 | Successfully edited item! 75 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | export default Todos; 82 | -------------------------------------------------------------------------------- /src/context/DeleteConfirmContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, ReactNode } from "react"; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | } 6 | 7 | interface DeleteConfirmInterface { 8 | isDeleteConfirmation: boolean; 9 | changeDeleteConfirm: () => void; 10 | } 11 | 12 | export const DeleteConfirmContext = createContext( 13 | null 14 | ); 15 | 16 | export const DeleteConfirmProvider = ({ children }: Props) => { 17 | const [isDeleteConfirmation, setIsDeleteConfirmation] = useState( 18 | JSON.parse(localStorage.getItem("deleteConfirmation")!) || false 19 | ); 20 | 21 | const changeDeleteConfirm = () => { 22 | localStorage.setItem("deleteConfirmation", String(!isDeleteConfirmation)); 23 | setIsDeleteConfirmation(!isDeleteConfirmation); 24 | }; 25 | 26 | const deleteConfirmValue: DeleteConfirmInterface = { 27 | isDeleteConfirmation, 28 | changeDeleteConfirm, 29 | }; 30 | 31 | return ( 32 | 33 | {children} 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/context/MainContext.tsx: -------------------------------------------------------------------------------- 1 | import { TodoType } from "../types"; 2 | import { createContext, useState, useEffect, ReactNode } from "react"; 3 | 4 | interface MainContextInterface { 5 | todos: TodoType[]; 6 | setTodos: React.Dispatch>; 7 | markComplete: (id: string) => void; 8 | delTodo: (id: string) => void; 9 | deleteAll: () => void; 10 | editTodo: (id: string, text: string) => void; 11 | addTodo: (title: string) => void; 12 | moveTodo: (old: number, new_: number) => void; 13 | markStar: (id: string) => void; 14 | } 15 | 16 | interface Props { 17 | children: ReactNode; 18 | } 19 | 20 | export const MainContext = createContext(null); 21 | 22 | export const MainProvider = ({ children }: Props) => { 23 | const [todos, setTodos] = useState( 24 | JSON.parse(localStorage.getItem("todos")!) || [] 25 | ); 26 | 27 | useEffect(() => { 28 | localStorage.setItem("todos", JSON.stringify(todos)); 29 | }, [todos]); 30 | 31 | const addTodo = (title: string) => { 32 | if (title.trim()) { 33 | const newTodo = { 34 | id: String(Math.random() * 5000), 35 | title, 36 | completed: false, 37 | starred: false, 38 | }; 39 | const orderTodos = [newTodo, ...todos]; 40 | orderStarAndComplete(orderTodos); 41 | setTodos(orderTodos); 42 | } 43 | }; 44 | const editTodo: (id: string, text: string) => void = ( 45 | id: string, 46 | text: string 47 | ) => { 48 | if (!(text === null) && text.trim()) { 49 | setTodos( 50 | todos.map((todo) => { 51 | if (todo.id === id) todo.title = text; 52 | return todo; 53 | }) 54 | ); 55 | } 56 | }; 57 | const markComplete = (id: string) => { 58 | const orderTodos = todos.map((todo) => { 59 | if (todo.id === id) todo.completed = !todo.completed; 60 | return todo; 61 | }); 62 | orderStarAndComplete(orderTodos); 63 | setTodos(orderTodos); 64 | }; 65 | 66 | const markStar = (id: string) => { 67 | const orderTodos = todos.map((todo) => { 68 | if (todo.id === id) todo.starred = !todo.starred; 69 | return todo; 70 | }); 71 | orderStarAndComplete(orderTodos); 72 | setTodos(orderTodos); 73 | }; 74 | 75 | const orderStarAndComplete = (todos: TodoType[]) => { 76 | todos.sort((x, y) => y.starred - x.starred); 77 | todos.sort((x, y) => x.completed - y.completed); 78 | }; 79 | 80 | const delTodo = (id: string) => 81 | setTodos(todos.filter((todo) => todo.id !== id)); 82 | const deleteAll = () => setTodos([]); 83 | const moveTodo = (old: number, new_: number) => { 84 | const copy = JSON.parse(JSON.stringify(todos)); 85 | const thing = JSON.parse(JSON.stringify(todos[old])); 86 | copy.splice(old, 1); 87 | copy.splice(new_, 0, thing); 88 | setTodos(copy); 89 | }; 90 | 91 | const mainContextValue: MainContextInterface = { 92 | todos, 93 | setTodos, 94 | markComplete, 95 | delTodo, 96 | deleteAll, 97 | editTodo, 98 | addTodo, 99 | moveTodo, 100 | markStar, 101 | }; 102 | 103 | return ( 104 | 105 | {children} 106 | 107 | ); 108 | }; 109 | -------------------------------------------------------------------------------- /src/context/SmallTextContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, ReactNode } from "react"; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | } 6 | 7 | interface SmallTextInterface { 8 | isSmallText: boolean; 9 | changeSmallText: () => void; 10 | } 11 | 12 | export const SmallTextContext = createContext(null); 13 | 14 | export const SmallTextProvider = ({ children }: Props) => { 15 | const [isSmallText, setIsSmallText] = useState( 16 | JSON.parse(localStorage.getItem("smallText")!) || false 17 | ); 18 | 19 | const changeSmallText = () => { 20 | window.localStorage.setItem("smallText", String(!isSmallText)); 21 | setIsSmallText(!isSmallText); 22 | }; 23 | 24 | const smallTextValue: SmallTextInterface = { 25 | isSmallText, 26 | changeSmallText, 27 | }; 28 | 29 | return ( 30 | 31 | {children} 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /src/context/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useEffect, useState, ReactNode } from "react"; 2 | import { 3 | enable as enableDarkMode, 4 | disable as disableDarkMode, 5 | } from "darkreader"; 6 | 7 | interface Props { 8 | children: ReactNode; 9 | } 10 | 11 | interface ThemeInterface { 12 | isDark: boolean; 13 | changeTheme: () => void; 14 | } 15 | 16 | export const ThemeContext = createContext(null); 17 | 18 | export const ThemeProvider = ({ children }: Props) => { 19 | const [isDark, setIsDark] = useState( 20 | JSON.parse(localStorage.getItem("darkTheme")!) || false 21 | ); 22 | 23 | const changeTheme = () => { 24 | setIsDark(!isDark); 25 | if (isDark) { 26 | enableDarkMode({ 27 | brightness: 100, 28 | contrast: 90, 29 | sepia: 10, 30 | }); 31 | } else { 32 | disableDarkMode(); 33 | } 34 | localStorage.setItem("darkTheme", String(isDark)); 35 | }; 36 | 37 | useEffect(() => { 38 | if (isDark) { 39 | enableDarkMode({ 40 | brightness: 100, 41 | contrast: 90, 42 | sepia: 10, 43 | }); 44 | } else disableDarkMode(); 45 | localStorage.setItem("darkTheme", JSON.stringify(isDark)); 46 | }, [isDark]); 47 | 48 | const themeValue: ThemeInterface = { 49 | isDark, 50 | changeTheme, 51 | }; 52 | 53 | return ( 54 | {children} 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/hooks/useChangeMenuIcon.tsx: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from "@material-ui/core"; 2 | import { MoreVert, MoreHoriz, SvgIconComponent } from "@material-ui/icons"; 3 | 4 | export default function useChangeMenuIcon(): SvgIconComponent { 5 | const Icon: SvgIconComponent = () => 6 | useMediaQuery("(max-width: 768px)") ? : ; 7 | return Icon; 8 | } 9 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "react-dom"; 2 | import { MainProvider } from "./context/MainContext"; 3 | import { ThemeProvider } from "./context/ThemeContext"; 4 | import { DeleteConfirmProvider } from "./context/DeleteConfirmContext"; 5 | import { SmallTextProvider } from "./context/SmallTextContext"; 6 | import App from "./App"; 7 | import "./styles.css"; 8 | import * as serviceWorkerRegistration from "./serviceWorkerRegistration"; 9 | 10 | render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | document.getElementById("root") 21 | ); 22 | 23 | // If you want your app to work offline and load faster, you can change 24 | // unregister() to register() below. Note this comes with some pitfalls. 25 | // Learn more about service workers: https://cra.link/PWA 26 | serviceWorkerRegistration.register(); 27 | -------------------------------------------------------------------------------- /src/pages/About.tsx: -------------------------------------------------------------------------------- 1 | import { Container } from "@material-ui/core"; 2 | import { 3 | Twitter, 4 | GitHub, 5 | Facebook, 6 | YouTube, 7 | Instagram, 8 | } from "@material-ui/icons"; 9 | 10 | const About = () => { 11 | return ( 12 | <> 13 | 14 |

15 | Max Todos is an open-source project started by Usman Sabuwala. 16 | This app focuses on the ease of use of a Todo App. Write your todos, 17 | change some settings, Enjoy! 18 |

19 |

Contact

20 | 30 | 31 | 32 | 42 | 43 | 44 | 54 | 55 | 56 | 66 | 67 | 68 | 78 | 79 | 80 |
81 | 82 | ); 83 | }; 84 | 85 | export default About; 86 | -------------------------------------------------------------------------------- /src/pages/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Switch, useMediaQuery } from "@material-ui/core"; 2 | import { useContext } from "react"; 3 | import { ThemeContext } from "../context/ThemeContext"; 4 | import { DeleteConfirmContext } from "../context/DeleteConfirmContext"; 5 | import { SmallTextContext } from "../context/SmallTextContext"; 6 | 7 | const Settings = () => { 8 | const { isDeleteConfirmation, changeDeleteConfirm } = useContext( 9 | DeleteConfirmContext 10 | )!; 11 | const { isDark, changeTheme } = useContext(ThemeContext)!; 12 | const { isSmallText, changeSmallText } = useContext(SmallTextContext)!; 13 | const matches = useMediaQuery("(max-width: 768px)"); 14 | 15 | return ( 16 | <> 17 | 18 |

19 | Dark Mode: 20 | 21 |

22 |

23 | Small Text Mode: 24 | 30 |

31 |

32 | Disable Delete Confirmation: 33 | 38 |

39 |
40 | 41 | ); 42 | }; 43 | 44 | export default Settings; 45 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/service-worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /* eslint-disable no-restricted-globals */ 3 | 4 | // This service worker can be customized! 5 | // See https://developers.google.com/web/tools/workbox/modules 6 | // for the list of available Workbox modules, or add any other 7 | // code you'd like. 8 | // You can also remove this file if you'd prefer not to use a 9 | // service worker, and the Workbox build step will be skipped. 10 | 11 | import { clientsClaim } from "workbox-core"; 12 | import { ExpirationPlugin } from "workbox-expiration"; 13 | import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching"; 14 | import { registerRoute } from "workbox-routing"; 15 | import { StaleWhileRevalidate } from "workbox-strategies"; 16 | 17 | declare const self: ServiceWorkerGlobalScope; 18 | 19 | clientsClaim(); 20 | 21 | // Precache all of the assets generated by your build process. 22 | // Their URLs are injected into the manifest variable below. 23 | // This variable must be present somewhere in your service worker file, 24 | // even if you decide not to use precaching. See https://cra.link/PWA 25 | precacheAndRoute(self.__WB_MANIFEST); 26 | 27 | // Set up App Shell-style routing, so that all navigation requests 28 | // are fulfilled with your index.html shell. Learn more at 29 | // https://developers.google.com/web/fundamentals/architecture/app-shell 30 | const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$"); 31 | registerRoute( 32 | // Return false to exempt requests from being fulfilled by index.html. 33 | ({ request, url }: { request: Request; url: URL }) => { 34 | // If this isn't a navigation, skip. 35 | if (request.mode !== "navigate") { 36 | return false; 37 | } 38 | 39 | // If this is a URL that starts with /_, skip. 40 | if (url.pathname.startsWith("/_")) { 41 | return false; 42 | } 43 | 44 | // If this looks like a URL for a resource, because it contains 45 | // a file extension, skip. 46 | if (url.pathname.match(fileExtensionRegexp)) { 47 | return false; 48 | } 49 | 50 | // Return true to signal that we want to use the handler. 51 | return true; 52 | }, 53 | createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html") 54 | ); 55 | 56 | // An example runtime caching route for requests that aren't handled by the 57 | // precache, in this case same-origin .png requests like those from in public/ 58 | registerRoute( 59 | // Add in any other file extensions or routing criteria as needed. 60 | ({ url }) => 61 | url.origin === self.location.origin && url.pathname.endsWith(".png"), 62 | // Customize this strategy as needed, e.g., by changing to CacheFirst. 63 | new StaleWhileRevalidate({ 64 | cacheName: "images", 65 | plugins: [ 66 | // Ensure that once this runtime cache reaches a maximum size the 67 | // least-recently used images are removed. 68 | new ExpirationPlugin({ maxEntries: 50 }), 69 | ], 70 | }) 71 | ); 72 | 73 | // This allows the web app to trigger skipWaiting via 74 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 75 | self.addEventListener("message", (event) => { 76 | if (event.data && event.data.type === "SKIP_WAITING") { 77 | self.skipWaiting(); 78 | } 79 | }); 80 | 81 | // Any other custom service worker logic can go here. 82 | -------------------------------------------------------------------------------- /src/serviceWorkerRegistration.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's 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/styles.css: -------------------------------------------------------------------------------- 1 | @media screen and (max-width: 768px) { 2 | .add-todo { 3 | margin-top: 10px; 4 | } 5 | .todo-text { 6 | font-size: 1.3rem !important; 7 | } 8 | .card-content { 9 | padding: 12px !important; 10 | } 11 | } 12 | 13 | .card-content { 14 | padding: 16px; 15 | } 16 | a { 17 | text-decoration: none; 18 | color: inherit; 19 | } 20 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface TodoType { 2 | id: string; 3 | title: string; 4 | completed: any; 5 | starred: any; 6 | } 7 | -------------------------------------------------------------------------------- /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-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------