├── .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 |
12 |
13 |
14 |
15 | [](#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 |
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 | You need to enable JavaScript to run this app.
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 |
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 |
40 | No
41 |
42 |
43 | Yes
44 |
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 |
61 | No
62 |
63 |
64 | Yes
65 |
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 |
45 | Cancel
46 |
47 | newValue.trim() && yes(newValue)}
49 | color="primary"
50 | variant="contained"
51 | >
52 | OK
53 |
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 |
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 |
201 | ) : (
202 |
203 | )
204 | ) : location === "/about" ? (
205 |
206 | ) : (
207 |
208 | )
209 | }
210 | style={{ color: "white", margin: 5 }}
211 | >
212 | {name}
213 |
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 |
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 |
--------------------------------------------------------------------------------