├── README.md ├── src ├── styles │ ├── Profile.module.css │ ├── Header.module.css │ ├── TodoItem.module.css │ ├── Login.module.css │ └── app.css ├── routes │ ├── NotMatch.jsx │ ├── Home.jsx │ ├── Profile.jsx │ ├── About.jsx │ ├── SinglePage.jsx │ └── Login.jsx ├── components │ ├── TodosLogic.jsx │ ├── Layout.jsx │ ├── TodosList.jsx │ ├── Header.jsx │ ├── ProtectedRoute.jsx │ ├── TodoApp.jsx │ ├── InputTodo.jsx │ ├── TodoItem.jsx │ └── Navbar.jsx ├── main.jsx └── context │ ├── AuthContext.jsx │ └── TodosContext.jsx ├── .babelrc ├── jsconfig.json ├── vite.config.js ├── .gitignore ├── index.html ├── .stylelintrc.json ├── .eslintrc.json ├── LICENSE.md ├── package.json ├── public └── vite.svg └── .github └── workflows └── linters.yml /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kifle23/react-todo-app/HEAD/README.md -------------------------------------------------------------------------------- /src/styles/Profile.module.css: -------------------------------------------------------------------------------- 1 | .profile { 2 | max-width: 1000px; 3 | margin: 0 auto; 4 | } 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ], 5 | "plugins": ["@babel/plugin-syntax-jsx"] 6 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | } -------------------------------------------------------------------------------- /src/routes/NotMatch.jsx: -------------------------------------------------------------------------------- 1 | const NotMatch = () => ( 2 |
3 |

Not found!

4 |
5 | ); 6 | export default NotMatch; 7 | -------------------------------------------------------------------------------- /src/styles/Header.module.css: -------------------------------------------------------------------------------- 1 | .header h1 { 2 | font-size: 3rem; 3 | font-weight: 600; 4 | margin-bottom: 1rem; 5 | line-height: 1em; 6 | text-transform: lowercase; 7 | text-align: center; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/TodosLogic.jsx: -------------------------------------------------------------------------------- 1 | import InputTodo from './InputTodo'; 2 | import TodosList from './TodosList'; 3 | 4 | import { TodosProvider } from '../context/TodosContext'; 5 | 6 | const TodosLogic = () => ( 7 | 8 | 9 | 10 | 11 | ); 12 | export default TodosLogic; 13 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | resolve: { 8 | alias: { 9 | '@': path.resolve(__dirname, './src'), 10 | }, 11 | }, 12 | plugins: [react()], 13 | }); 14 | -------------------------------------------------------------------------------- /src/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | import { Outlet } from 'react-router-dom'; 2 | import Navbar from './Navbar'; 3 | import { AuthProvider } from '../context/AuthContext'; 4 | 5 | const Layout = () => ( 6 |
7 | 8 | 9 | 10 | 11 |
12 | ); 13 | export default Layout; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/components/TodosList.jsx: -------------------------------------------------------------------------------- 1 | import TodoItem from './TodoItem'; 2 | import { useTodosContext } from '../context/TodosContext'; 3 | 4 | const TodosList = () => { 5 | const { todos } = useTodosContext(); 6 | return ( 7 | 12 | ); 13 | }; 14 | export default TodosList; 15 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/routes/Home.jsx: -------------------------------------------------------------------------------- 1 | import Header from '../components/Header'; 2 | import TodosLogic from '../components/TodosLogic'; 3 | 4 | const Home = () => ( 5 | //
remove the div 6 |
7 |
8 |

todos

9 |

Items will persist in the browser local storage

10 |
11 | 12 |
13 | //
14 | ); 15 | export default Home; 16 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | 5 | import TodoApp from './components/TodoApp'; 6 | 7 | import '@/styles/app.css'; 8 | // import './styles/app.css'; 9 | 10 | const domContainer = document.getElementById('root'); 11 | const root = ReactDOM.createRoot(domContainer); 12 | root.render( 13 | 14 | 15 | 16 | 17 | , 18 | ); 19 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import styles from '@/styles/Header.module.css'; 3 | 4 | const Header = ({ children }) => { 5 | const headerStyle = { 6 | padding: '20px 0', 7 | lineHeight: '1.5em', 8 | color: '#aeadad', 9 | textAlign: 'center', 10 | }; 11 | return ( 12 |
13 | {children} 14 |
15 | ); 16 | }; 17 | 18 | Header.propTypes = { 19 | children: PropTypes.node.isRequired, 20 | }; 21 | 22 | export default Header; 23 | -------------------------------------------------------------------------------- /src/routes/Profile.jsx: -------------------------------------------------------------------------------- 1 | import Header from '../components/Header'; 2 | import { useAuthContext } from '../context/AuthContext'; 3 | import styles from '@/styles/Profile.module.css'; 4 | 5 | const Profile = () => { 6 | const { user } = useAuthContext(); 7 | return ( 8 |
9 |
10 |

Profile.

11 |
12 |
13 |

14 | Hello, 15 | {user} 16 |

17 |
18 |
19 | ); 20 | }; 21 | 22 | export default Profile; 23 | -------------------------------------------------------------------------------- /src/routes/About.jsx: -------------------------------------------------------------------------------- 1 | import { NavLink, Outlet } from 'react-router-dom'; 2 | import Header from '../components/Header'; 3 | 4 | const About = () => ( 5 | <> 6 |
7 |

About page.

8 |
9 |
10 | 18 | 19 |
20 | 21 | ); 22 | export default About; 23 | -------------------------------------------------------------------------------- /src/components/ProtectedRoute.jsx: -------------------------------------------------------------------------------- 1 | import { Navigate, useLocation } from 'react-router-dom'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import { useAuthContext } from '../context/AuthContext'; 5 | 6 | const ProtectedRoute = ({ children }) => { 7 | const { user } = useAuthContext(); 8 | const location = useLocation(); 9 | 10 | if (!user) { 11 | return ( 12 | 13 | ); 14 | } 15 | return children; 16 | }; 17 | ProtectedRoute.propTypes = { 18 | children: PropTypes.node.isRequired, 19 | }; 20 | 21 | export default ProtectedRoute; 22 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard"], 3 | "plugins": ["stylelint-scss", "stylelint-csstree-validator"], 4 | "rules": { 5 | "at-rule-no-unknown": [ 6 | true, 7 | { 8 | "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"] 9 | } 10 | ], 11 | "scss/at-rule-no-unknown": [ 12 | true, 13 | { 14 | "ignoreAtRules": ["tailwind", "apply", "variants", "responsive", "screen"] 15 | } 16 | ], 17 | "csstree/validator": true 18 | }, 19 | "ignoreFiles": ["build/**", "dist/**", "**/reset*.css", "**/bootstrap*.css", "**/*.js", "**/*.jsx"] 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/TodoItem.module.css: -------------------------------------------------------------------------------- 1 | .item { 2 | font-size: 1rem; 3 | list-style-type: none; 4 | padding: 14px 25px; 5 | border-bottom: 1px solid #fff; 6 | background: #f0efef; 7 | } 8 | 9 | .content { 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | gap: 1rem; 14 | } 15 | 16 | .content span { 17 | flex: 1; 18 | order: 1; 19 | } 20 | 21 | .content button { 22 | cursor: pointer; 23 | background: transparent; 24 | order: 2; 25 | outline: none; 26 | border: none; 27 | margin-left: 7px; 28 | } 29 | 30 | .textInput { 31 | width: 100%; 32 | padding: 10px; 33 | border: 1px solid #dfdfdf; 34 | outline: none; 35 | } 36 | -------------------------------------------------------------------------------- /src/styles/Login.module.css: -------------------------------------------------------------------------------- 1 | .formWrapper { 2 | max-width: 1000px; 3 | margin: 0 auto; 4 | } 5 | 6 | .form { 7 | width: 100%; 8 | max-width: 500px; 9 | margin: 0 auto; 10 | display: flex; 11 | flex-direction: column; 12 | box-shadow: rgb(32 32 32 / 8%) 0 10px 32px 0; 13 | padding: 40px; 14 | } 15 | 16 | .form input { 17 | height: 50px; 18 | width: 100%; 19 | padding: 10px 20px; 20 | font-size: 15px; 21 | margin-bottom: 10px; 22 | outline: none; 23 | border-radius: 2px; 24 | border: 1px solid rgb(209, 215, 217); 25 | } 26 | 27 | .form button { 28 | border-radius: 2px; 29 | border: none; 30 | background-color: #758488; 31 | font-size: 18px; 32 | width: 100%; 33 | height: 50px; 34 | color: rgb(255, 255, 255); 35 | text-transform: uppercase; 36 | padding: 0 20px; 37 | cursor: pointer; 38 | } 39 | -------------------------------------------------------------------------------- /src/routes/SinglePage.jsx: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router-dom'; 2 | 3 | const aboutData = [ 4 | { 5 | slug: 'about-app', 6 | title: 'About the app', 7 | description: 8 | "This application lets us add to-dos, edit, and delete items. Log in to see the delete feature. It also persists to-dos in the browser's local storage for a subsequent visit.", 9 | }, 10 | { 11 | slug: 'about-developer', 12 | title: 'About the developer', 13 | description: 14 | 'Ibas Majid founded ibaslogic.com to experiment with new web features and write actionable guides. Follow Ibas on Twitter @ibaslogic to learn modern web development.', 15 | }, 16 | ]; 17 | 18 | const SinglePage = () => { 19 | const { slug } = useParams(); 20 | const aboutContent = aboutData.find((item) => item.slug === slug); 21 | const { title, description } = aboutContent; 22 | return ( 23 |
24 |

{title}

25 |

{description}

26 |
27 | ); 28 | }; 29 | export default SinglePage; 30 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parser": "@babel/eslint-parser", 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "extends": [ 16 | "airbnb", 17 | "plugin:react/recommended", 18 | "plugin:react-hooks/recommended" 19 | ], 20 | "plugins": ["react"], 21 | "rules": { 22 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }], 23 | "react/react-in-jsx-scope": "off", 24 | "import/no-unresolved": "off", 25 | "no-shadow": "off" 26 | }, 27 | "overrides": [ 28 | { 29 | // feel free to replace with your preferred file pattern - eg. 'src/**/*Slice.js' or 'redux/**/*Slice.js' 30 | "files": ["src/**/*Slice.js"], 31 | // avoid state param assignment 32 | "rules": { "no-param-reassign": ["error", { "props": false }] } 33 | } 34 | ], 35 | "ignorePatterns": ["dist/", "build/"] 36 | } 37 | -------------------------------------------------------------------------------- /src/components/TodoApp.jsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | 3 | import Home from '../routes/Home'; 4 | import About from '../routes/About'; 5 | import Login from '../routes/Login'; 6 | import Profile from '../routes/Profile'; 7 | import NotMatch from '../routes/NotMatch'; 8 | import Layout from './Layout'; 9 | import SinglePage from '../routes/SinglePage'; 10 | import ProtectedRoute from './ProtectedRoute'; 11 | 12 | const TodoApp = () => ( 13 | 14 | }> 15 | } /> 16 | }> 17 | } /> 18 | 19 | } /> 20 | 24 | 25 | 26 | )} 27 | /> 28 | } /> 29 | 30 | 31 | ); 32 | export default TodoApp; 33 | -------------------------------------------------------------------------------- /src/context/AuthContext.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { 3 | useState, useContext, createContext, useEffect, 4 | } from 'react'; 5 | 6 | const AuthContext = createContext(null); 7 | 8 | export const AuthProvider = ({ children }) => { 9 | function getUsername() { 10 | // getting stored state 11 | const temp = localStorage.getItem('username'); 12 | const savedUsername = JSON.parse(temp); 13 | return savedUsername || ''; 14 | } 15 | AuthProvider.propTypes = { 16 | children: PropTypes.node.isRequired, 17 | }; 18 | const [user, setUser] = useState(getUsername()); 19 | 20 | useEffect(() => { 21 | // storing user state 22 | const temp = JSON.stringify(user); 23 | localStorage.setItem('username', temp); 24 | }, [user]); 25 | 26 | const login = (user) => setUser(user); 27 | const logout = () => setUser(null); 28 | 29 | return ( 30 | 31 | {children} 32 | 33 | ); 34 | }; 35 | 36 | export const useAuthContext = () => useContext(AuthContext); 37 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kifle Haile 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-todo-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@vitejs/plugin-react": "^3.0.0", 13 | "prop-types": "^15.8.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "react-icons": "^4.7.1", 17 | "react-router-dom": "^6.6.2", 18 | "uuid": "^9.0.0", 19 | "vite": "^4.0.0" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.22.1", 23 | "@babel/eslint-parser": "^7.21.8", 24 | "@babel/plugin-syntax-jsx": "^7.21.4", 25 | "@babel/preset-react": "^7.22.3", 26 | "@types/react": "^18.0.26", 27 | "@types/react-dom": "^18.0.9", 28 | "eslint": "^7.32.0", 29 | "eslint-config-airbnb": "^18.2.1", 30 | "eslint-plugin-import": "^2.27.5", 31 | "eslint-plugin-jsx-a11y": "^6.7.1", 32 | "eslint-plugin-react": "^7.32.2", 33 | "eslint-plugin-react-hooks": "^4.6.0", 34 | "stylelint": "^13.13.1", 35 | "stylelint-config-standard": "^21.0.0", 36 | "stylelint-csstree-validator": "^1.9.0", 37 | "stylelint-scss": "^3.21.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useLocation, useNavigate } from 'react-router-dom'; 3 | 4 | import styles from '../styles/Login.module.css'; 5 | import { useAuthContext } from '../context/AuthContext'; 6 | import Header from '../components/Header'; 7 | 8 | const Login = () => { 9 | const [username, setUsername] = useState(''); 10 | const { login } = useAuthContext(); 11 | 12 | const navigate = useNavigate(); 13 | const location = useLocation(); 14 | 15 | const from = location.state?.pathname || '/'; 16 | 17 | const handleSubmit = (e) => { 18 | e.preventDefault(); 19 | if (!username) return; 20 | login(username); 21 | setUsername(''); 22 | navigate(from, { replace: true }); 23 | }; 24 | 25 | return ( 26 |
27 |
28 |

Login

29 |
30 |
31 |
32 | setUsername(e.target.value)} 37 | /> 38 | 39 |
40 |
41 |
42 | ); 43 | }; 44 | 45 | export default Login; 46 | -------------------------------------------------------------------------------- /src/components/InputTodo.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { FaPlusCircle } from 'react-icons/fa'; 3 | import { useTodosContext } from '../context/TodosContext'; 4 | 5 | const InputTodo = () => { 6 | const [title, setTitle] = useState(''); 7 | const [message, setMessage] = useState(''); 8 | 9 | const { addTodoItem } = useTodosContext(); 10 | 11 | const handleChange = (e) => { 12 | setTitle(e.target.value); 13 | }; 14 | 15 | const handleSubmit = (e) => { 16 | e.preventDefault(); 17 | if (title.trim()) { 18 | addTodoItem(title); 19 | setTitle(''); 20 | setMessage(''); 21 | } else { 22 | setMessage('Please add item.'); 23 | } 24 | }; 25 | 26 | return ( 27 | <> 28 |
29 | 36 | 45 |
46 | {message} 47 | 48 | ); 49 | }; 50 | export default InputTodo; 51 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/context/TodosContext.jsx: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import { 3 | useState, useEffect, createContext, useContext, 4 | } from 'react'; 5 | 6 | import { v4 as uuidv4 } from 'uuid'; 7 | 8 | const TodosContext = createContext(null); 9 | 10 | export const TodosProvider = ({ children }) => { 11 | function getInitialTodos() { 12 | // getting stored items 13 | const temp = localStorage.getItem('todos'); 14 | const savedTodos = JSON.parse(temp); 15 | return savedTodos || []; 16 | } 17 | TodosProvider.propTypes = { 18 | children: PropTypes.node.isRequired, 19 | }; 20 | 21 | const [todos, setTodos] = useState(getInitialTodos()); 22 | 23 | useEffect(() => { 24 | // storing todos items 25 | const temp = JSON.stringify(todos); 26 | localStorage.setItem('todos', temp); 27 | }, [todos]); 28 | 29 | const handleChange = (id) => { 30 | setTodos((prevState) => prevState.map((todo) => { 31 | if (todo.id === id) { 32 | return { 33 | ...todo, 34 | completed: !todo.completed, 35 | }; 36 | } 37 | return todo; 38 | })); 39 | }; 40 | 41 | const delTodo = (id) => { 42 | setTodos([...todos.filter((todo) => todo.id !== id)]); 43 | }; 44 | 45 | const addTodoItem = (title) => { 46 | const newTodo = { 47 | id: uuidv4(), 48 | title, 49 | completed: false, 50 | }; 51 | setTodos([...todos, newTodo]); 52 | }; 53 | 54 | const setUpdate = (updatedTitle, id) => { 55 | setTodos( 56 | todos.map((todo) => { 57 | if (todo.id === id) { 58 | return { 59 | ...todo, 60 | title: updatedTitle, 61 | }; 62 | } 63 | return todo; 64 | }), 65 | ); 66 | }; 67 | return ( 68 | 77 | {children} 78 | 79 | ); 80 | }; 81 | export const useTodosContext = () => useContext(TodosContext); 82 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: pull_request 4 | 5 | env: 6 | FORCE_COLOR: 1 7 | 8 | jobs: 9 | eslint: 10 | name: ESLint 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: "18.x" 17 | - name: Setup ESLint 18 | run: | 19 | npm install --save-dev eslint@7.x eslint-config-airbnb@18.x eslint-plugin-import@2.x eslint-plugin-jsx-a11y@6.x eslint-plugin-react@7.x eslint-plugin-react-hooks@4.x @babel/eslint-parser@7.x @babel/core@7.x @babel/plugin-syntax-jsx@7.x @babel/preset-env@7.x @babel/preset-react@7.x 20 | [ -f .eslintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.eslintrc.json 21 | [ -f .babelrc ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.babelrc 22 | - name: ESLint Report 23 | run: npx eslint "**/*.{js,jsx}" 24 | stylelint: 25 | name: Stylelint 26 | runs-on: ubuntu-22.04 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: "18.x" 32 | - name: Setup Stylelint 33 | run: | 34 | npm install --save-dev stylelint@13.x stylelint-scss@3.x stylelint-config-standard@21.x stylelint-csstree-validator@1.x 35 | [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/react-redux/.stylelintrc.json 36 | - name: Stylelint Report 37 | run: npx stylelint "**/*.{css,scss}" 38 | nodechecker: 39 | name: node_modules checker 40 | runs-on: ubuntu-22.04 41 | steps: 42 | - uses: actions/checkout@v3 43 | - name: Check node_modules existence 44 | run: | 45 | if [ -d "node_modules/" ]; then echo -e "\e[1;31mThe node_modules/ folder was pushed to the repo. Please remove it from the GitHub repository and try again."; echo -e "\e[1;32mYou can set up a .gitignore file with this folder included on it to prevent this from happening in the future." && exit 1; fi 46 | -------------------------------------------------------------------------------- /src/components/TodoItem.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import { FaTrash } from 'react-icons/fa'; 3 | import { AiFillEdit } from 'react-icons/ai'; 4 | import PropTypes from 'prop-types'; 5 | import { useTodosContext } from '../context/TodosContext'; 6 | import { useAuthContext } from '../context/AuthContext'; 7 | import styles from '@/styles/TodoItem.module.css'; 8 | 9 | const TodoItem = ({ itemProp }) => { 10 | TodoItem.propTypes = { 11 | itemProp: PropTypes.node.isRequired, 12 | }; 13 | const [editing, setEditing] = useState(false); 14 | 15 | const { handleChange, delTodo, setUpdate } = useTodosContext(); 16 | const { user } = useAuthContext(); 17 | 18 | const editInputRef = useRef(null); 19 | 20 | const completedStyle = { 21 | fontStyle: 'italic', 22 | color: '#595959', 23 | opacity: 0.4, 24 | textDecoration: 'line-through', 25 | }; 26 | 27 | const handleEditing = () => { 28 | setEditing(true); 29 | }; 30 | 31 | const viewMode = {}; 32 | const editMode = {}; 33 | if (editing) { 34 | viewMode.display = 'none'; 35 | } else { 36 | editMode.display = 'none'; 37 | } 38 | 39 | const handleUpdatedDone = (event) => { 40 | if (event.key === 'Enter') { 41 | setUpdate(editInputRef.current.value, itemProp.id); 42 | setEditing(false); 43 | } 44 | }; 45 | return ( 46 |
  • 47 |
    48 | handleChange(itemProp.id)} 52 | /> 53 | {user && ( 54 | 57 | )} 58 | 61 | 62 | {itemProp.title} 63 | 64 |
    65 | 73 |
  • 74 | ); 75 | }; 76 | export default TodoItem; 77 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | import { NavLink, useNavigate } from 'react-router-dom'; 3 | 4 | import { MdClose } from 'react-icons/md'; 5 | import { FiMenu } from 'react-icons/fi'; 6 | import { useAuthContext } from '../context/AuthContext'; 7 | 8 | const links = [ 9 | { path: '/', text: 'Home' }, 10 | { path: 'about', text: 'About' }, 11 | { path: 'profile', text: 'Profile' }, 12 | { path: 'login', text: 'Login' }, 13 | ]; 14 | const Navbar = () => { 15 | const [navbarOpen, setNavbarOpen] = useState(false); 16 | const { user, logout } = useAuthContext(); 17 | 18 | const ref = useRef(); 19 | 20 | const navigate = useNavigate(); 21 | 22 | const handleLogout = () => { 23 | logout(); 24 | navigate('/login'); 25 | }; 26 | 27 | useEffect(() => { 28 | const handler = (event) => { 29 | if (navbarOpen && ref.current && !ref.current.contains(event.target)) { 30 | setNavbarOpen(false); 31 | } 32 | }; 33 | document.addEventListener('mousedown', handler); 34 | 35 | return () => { 36 | // Cleanup the event listener 37 | document.removeEventListener('mousedown', handler); 38 | }; 39 | }, [navbarOpen]); 40 | 41 | return ( 42 | <> 43 | 95 | 96 | {user && ( 97 |
    98 |

    {user}

    99 | 102 |
    103 | )} 104 | 105 | ); 106 | }; 107 | export default Navbar; 108 | -------------------------------------------------------------------------------- /src/styles/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font-family: "Segoe UI", sans-serif; 9 | line-height: 1.4; 10 | color: #000; 11 | background: #fff; 12 | height: 100vh; 13 | font-weight: 400; 14 | } 15 | 16 | .wrapper { 17 | width: 100%; 18 | padding: 8rem 20px 3rem; 19 | } 20 | 21 | .todos { 22 | max-width: 580px; 23 | margin: 0 auto; 24 | } 25 | 26 | .form-container { 27 | display: flex; 28 | border-radius: calc(0.1 * 100px); 29 | box-shadow: 0 4px 14px 0 rgba(70, 70, 70, 0.38); 30 | justify-content: space-around; 31 | background: #fff; 32 | font-size: 1rem; 33 | } 34 | 35 | .input-text { 36 | width: 85%; 37 | padding-right: 5px; 38 | padding-left: 10px; 39 | border-radius: calc(0.5 * 100px); 40 | font-size: 1rem; 41 | } 42 | 43 | .input-text::placeholder { 44 | color: #000; 45 | } 46 | 47 | .input-submit { 48 | background: transparent; 49 | color: #5b5b5b; 50 | text-transform: capitalize; 51 | cursor: pointer; 52 | font-weight: 600; 53 | margin-right: 10px; 54 | } 55 | 56 | .input-text, 57 | .input-submit { 58 | height: 45px; 59 | outline: none; 60 | border: none; 61 | } 62 | 63 | .submit-warning { 64 | font-size: 13px; 65 | color: red; 66 | margin-top: 5px; 67 | display: block; 68 | } 69 | 70 | a { 71 | color: #333; 72 | text-decoration: none; 73 | } 74 | 75 | /* Active route */ 76 | .active { 77 | color: #8a4baf; 78 | } 79 | 80 | /* ========================== 81 | About route 82 | ======================== */ 83 | 84 | .about { 85 | max-width: 1000px; 86 | margin: 0 auto; 87 | } 88 | 89 | .about_list { 90 | list-style: none; 91 | } 92 | 93 | .about_list a { 94 | padding: 20px 0; 95 | display: inline-block; 96 | text-decoration: none; 97 | font-weight: bold; 98 | } 99 | 100 | .menu-nav li a, 101 | .menu-nav li span { 102 | display: block; 103 | padding: 1rem; 104 | } 105 | 106 | .about_list a:hover { 107 | color: #8a4baf; 108 | } 109 | 110 | .main_content { 111 | max-width: 650px; 112 | } 113 | 114 | .main_content h2 { 115 | margin-bottom: 15px; 116 | line-height: 30px; 117 | font-size: 26px; 118 | } 119 | 120 | .main_content p { 121 | line-height: 25px; 122 | } 123 | 124 | /* ========================== 125 | Sidebar navigation 126 | ======================== */ 127 | 128 | .navbar { 129 | position: relative; 130 | } 131 | 132 | button { 133 | cursor: pointer; 134 | background: transparent; 135 | border: none; 136 | font-size: 20px; 137 | } 138 | 139 | .navbar .toggle { 140 | position: fixed; 141 | left: 30px; 142 | top: 40px; 143 | cursor: pointer; 144 | background: transparent; 145 | border: none; 146 | } 147 | 148 | .menu-nav { 149 | list-style: none; 150 | position: absolute; 151 | background: #fff; 152 | left: 0; 153 | width: 0; 154 | overflow: hidden; 155 | max-width: 290px; 156 | z-index: 9; 157 | font-size: 18px; 158 | box-shadow: 0 10px 15px -3px rgb(46 41 51 / 8%), 0 4px 6px -2px rgb(71 63 79 / 16%); 159 | transform: translateX(-100px); 160 | transition: transform ease-in-out 0.2s; 161 | 162 | /* transition: width ease 0.2s; */ 163 | } 164 | 165 | .menu-nav.show-menu { 166 | width: 100%; 167 | transform: translateX(0); 168 | } 169 | 170 | .log-in { 171 | color: #777; 172 | border-top: 1px solid #efecec; 173 | margin-top: 15px; 174 | font-size: 15px; 175 | } 176 | 177 | /* logout */ 178 | .logout { 179 | position: absolute; 180 | right: 30px; 181 | top: 40px; 182 | display: flex; 183 | gap: 2rem; 184 | font-size: 20px; 185 | color: #8a4baf; 186 | } 187 | 188 | /* Media Queries */ 189 | @media screen and (min-width: 480px) { 190 | div.wrapper h1 { 191 | font-size: 6rem; 192 | } 193 | 194 | .about { 195 | display: grid; 196 | grid-template-columns: 200px minmax(0, 1fr); 197 | } 198 | } 199 | --------------------------------------------------------------------------------