├── 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 |
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 |
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 |
8 | {todos.map((todo) => (
9 |
10 | ))}
11 |
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 |
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 |
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 |
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 |
9 |
10 |
11 | -
12 | About app
13 |
14 | -
15 | About developer
16 |
17 |
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 |
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 |
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 |
--------------------------------------------------------------------------------