├── README.md ├── public ├── favicon.ico ├── robots.txt ├── manifest.json └── index.html ├── src ├── styles │ ├── _mixins.scss │ ├── modules │ │ ├── title.module.scss │ │ ├── button.module.scss │ │ ├── app.module.scss │ │ ├── modal.module.scss │ │ └── todoItem.module.scss │ └── GlobalStyles.css ├── utils │ └── getClasses.js ├── app │ └── store.js ├── components │ ├── PageTitle.js │ ├── Button.js │ ├── CheckButton.js │ ├── AppHeader.js │ ├── AppContent.js │ ├── TodoItem.js │ └── TodoModal.js ├── index.js ├── App.js └── slices │ └── todoSlice.js ├── .gitignore └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # React-Note 2 | Simple React Note App (CRUD, Search and Sort features Available) 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whitehorse21/React-Note/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin smallDeviceSize { 2 | @media only screen and (max-width: 768px) { 3 | @content; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/getClasses.js: -------------------------------------------------------------------------------- 1 | export const getClasses = (classes) => 2 | classes 3 | .filter((item) => item !== '') 4 | .join(' ') 5 | .trim(); 6 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Todo App", 3 | "name": "Todo App - by Shaif Arfan", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /src/app/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import todoReducer from '../slices/todoSlice'; 3 | 4 | export const store = configureStore({ 5 | reducer: { 6 | todo: todoReducer, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/PageTitle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from '../styles/modules/title.module.scss'; 3 | 4 | function PageTitle({ children, ...rest }) { 5 | return ( 6 |

7 | {children} 8 |

9 | ); 10 | } 11 | 12 | export default PageTitle; 13 | -------------------------------------------------------------------------------- /src/styles/modules/title.module.scss: -------------------------------------------------------------------------------- 1 | @import "../mixins"; 2 | .title { 3 | display: inline-block; 4 | width: 100%; 5 | font-family: "Poppins"; 6 | font-size: 4rem; 7 | font-weight: 700; 8 | text-transform: uppercase; 9 | text-align: center; 10 | margin: 0 auto; 11 | margin-top: 2rem; 12 | margin-bottom: 1.5rem; 13 | color: var(--black-1); 14 | @include smallDeviceSize { 15 | font-size: 3rem; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.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 | 25 | # Local Netlify folder 26 | .netlify 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import App from "./App"; 5 | import "@fontsource/poppins"; 6 | import "@fontsource/poppins/500.css"; 7 | import "@fontsource/poppins/600.css"; 8 | import "@fontsource/poppins/700.css"; 9 | import "./styles/GlobalStyles.css"; 10 | import { store } from "./app/store"; 11 | 12 | ReactDOM.render( 13 | 14 | 15 | 16 | 17 | , 18 | document.getElementById("root") 19 | ); 20 | -------------------------------------------------------------------------------- /src/styles/GlobalStyles.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::after, 3 | *::before { 4 | box-sizing: border-box; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | :root { 9 | --primaryPurple: #646ff0; 10 | --black-1: #646681; 11 | --black-2: #585858; 12 | --bg-1: #f8f8ff; 13 | --bg-2: #ecedf6; 14 | --bg-3: #cccdde; 15 | --gray-1: #eee; 16 | --gray-2: #dedfe1; 17 | --black: black; 18 | --white: white; 19 | } 20 | html { 21 | font-size: 10px; 22 | } 23 | body { 24 | font-family: 'Poppins', sans-serif; 25 | width: 100%; 26 | min-height: 100vh; 27 | background-color: var(--bg-1); 28 | } 29 | * { 30 | font-family: 'Poppins', sans-serif; 31 | } 32 | .container { 33 | width: 90%; 34 | max-width: 1200px; 35 | margin: 0 auto; 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/modules/button.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: inline-block; 3 | height: auto; 4 | padding: 0.8rem 2rem; 5 | border: none; 6 | border-radius: 6px; 7 | font-weight: 500; 8 | font-size: 1.6rem; 9 | text-decoration: none; 10 | text-transform: capitalize; 11 | cursor: pointer; 12 | overflow: hidden; 13 | &__select { 14 | color: var(--black-2); 15 | font-family: Poppins; 16 | padding: 1rem; 17 | border: none; 18 | background-color: var(--bg-3); 19 | 20 | width: 160px; 21 | cursor: pointer; 22 | } 23 | } 24 | .button--primary { 25 | background-color: var(--primaryPurple); 26 | color: var(--white); 27 | } 28 | .button--secondary { 29 | background-color: var(--bg-3); 30 | color: var(--black-1); 31 | } 32 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Toaster } from "react-hot-toast"; 3 | import AppContent from "./components/AppContent"; 4 | import AppHeader from "./components/AppHeader"; 5 | import PageTitle from "./components/PageTitle"; 6 | import styles from "./styles/modules/app.module.scss"; 7 | 8 | function App() { 9 | return ( 10 | <> 11 |
12 | Note List 13 |
14 | 15 | 16 |
17 |
18 | 26 | 27 | ); 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /src/components/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from '../styles/modules/button.module.scss'; 3 | import { getClasses } from '../utils/getClasses'; 4 | 5 | const buttonTypes = { 6 | primary: 'primary', 7 | secondary: 'secondary', 8 | }; 9 | 10 | function Button({ type, variant = 'primary', children, ...rest }) { 11 | return ( 12 | 22 | ); 23 | } 24 | 25 | function SelectButton({ children, id, ...rest }) { 26 | return ( 27 | 34 | ); 35 | } 36 | 37 | export { SelectButton }; 38 | export default Button; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fontsource/poppins": "^4.5.0", 7 | "@reduxjs/toolkit": "^1.7.1", 8 | "date-fns": "^2.28.0", 9 | "framer-motion": "^5.5.1", 10 | "node-sass": "^7.0.0", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-hot-toast": "^2.2.0", 14 | "react-icons": "^4.3.1", 15 | "react-redux": "^7.2.6", 16 | "react-scripts": "5.0.0", 17 | "uuid": "^8.3.2" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "eject": "react-scripts eject" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "wesbos" 28 | ], 29 | "rules": { 30 | "import/no-extraneous-dependencies": [ 31 | "error", 32 | { 33 | "devDependencies": true 34 | } 35 | ], 36 | "react/prop-types": 0 37 | } 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "eslint-config-wesbos": "^3.0.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/styles/modules/app.module.scss: -------------------------------------------------------------------------------- 1 | @import "../_mixins.scss"; 2 | 3 | .app__wrapper { 4 | max-width: 750px; 5 | width: 100%; 6 | margin: 0 auto; 7 | } 8 | .appHeader { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | justify-content: center; 13 | } 14 | .topHeader { 15 | width: 100%; 16 | display: flex; 17 | align-items: center; 18 | justify-content: space-between; 19 | height: 60px; 20 | 21 | label { 22 | font-size: 1.6rem; 23 | color: var(--black-1); 24 | } 25 | } 26 | .searchHeader { 27 | width: 100%; 28 | input { 29 | margin-top: 0.5rem; 30 | margin-bottom: 2rem; 31 | width: 100%; 32 | padding: 1rem; 33 | border: none; 34 | background-color: var(--white); 35 | font-size: 1.6rem; 36 | } 37 | } 38 | .content__wrapper { 39 | background-color: var(--bg-2); 40 | padding: 2rem; 41 | border-radius: 12px; 42 | @include smallDeviceSize { 43 | padding: 1.5rem; 44 | } 45 | } 46 | .emptyText { 47 | // display: inline-block; 48 | font-size: 1.6rem; 49 | font-weight: 500; 50 | color: var(--black-2); 51 | text-align: center; 52 | margin: 0 auto; 53 | padding: 0.5rem 1rem; 54 | border-radius: 8px; 55 | background-color: var(--gray-2); 56 | width: max-content; 57 | height: auto; 58 | } 59 | -------------------------------------------------------------------------------- /src/components/CheckButton.js: -------------------------------------------------------------------------------- 1 | import { motion, useMotionValue, useTransform } from 'framer-motion'; 2 | import React from 'react'; 3 | import styles from '../styles/modules/todoItem.module.scss'; 4 | 5 | const checkVariants = { 6 | initial: { 7 | color: '#fff', 8 | }, 9 | checked: { pathLength: 1 }, 10 | unchecked: { pathLength: 0 }, 11 | }; 12 | 13 | const boxVariants = { 14 | checked: { 15 | background: 'var(--primaryPurple)', 16 | transition: { duration: 0.1 }, 17 | }, 18 | unchecked: { background: 'var(--gray-2)', transition: { duration: 0.1 } }, 19 | }; 20 | 21 | function CheckButton({ checked, handleCheck }) { 22 | const pathLength = useMotionValue(0); 23 | const opacity = useTransform(pathLength, [0.05, 0.15], [0, 1]); 24 | 25 | return ( 26 | handleCheck()} 31 | > 32 | 38 | 49 | 50 | 51 | ); 52 | } 53 | 54 | export default CheckButton; 55 | -------------------------------------------------------------------------------- /src/styles/modules/modal.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | background: rgba(0, 0, 0, 0.5); 8 | z-index: 1000; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | .container { 14 | background-color: var(--bg-2); 15 | max-width: 500px; 16 | width: 90%; 17 | margin: 0 auto; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | padding: 2rem; 22 | border-radius: 8px; 23 | position: relative; 24 | } 25 | .closeButton { 26 | position: absolute; 27 | top: -10px; 28 | right: 0; 29 | transform: translateY(-100%); 30 | font-size: 2.5rem; 31 | padding: 0.5rem; 32 | border-radius: 4px; 33 | background-color: var(--gray-1); 34 | color: var(--black-2); 35 | display: flex; 36 | align-items: center; 37 | justify-content: center; 38 | cursor: pointer; 39 | transition: 0.3s ease all; 40 | z-index: -1; 41 | &:hover { 42 | background-color: #e32525; 43 | color: white; 44 | } 45 | } 46 | .formTitle { 47 | color: var(--black-1); 48 | font-size: 2rem; 49 | font-weight: 600; 50 | margin-bottom: 2rem; 51 | text-transform: capitalize; 52 | } 53 | .form { 54 | width: 100%; 55 | label { 56 | font-size: 1.6rem; 57 | color: var(--black-1); 58 | input, 59 | textarea { 60 | margin-top: 0.5rem; 61 | margin-bottom: 2rem; 62 | width: 100%; 63 | padding: 1rem; 64 | border: none; 65 | background-color: var(--white); 66 | font-size: 1.6rem; 67 | } 68 | } 69 | } 70 | .buttonContainer { 71 | display: flex; 72 | justify-content: flex-start; 73 | align-items: center; 74 | margin-top: 2rem; 75 | gap: 1rem; 76 | } 77 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 25 | Todo App 26 | 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/styles/modules/todoItem.module.scss: -------------------------------------------------------------------------------- 1 | @import "../_mixins.scss"; 2 | 3 | .item { 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | padding: 1rem; 8 | background: var(--white); 9 | margin-bottom: 1.5rem; 10 | border-radius: 4px; 11 | &:last-child { 12 | margin-bottom: 0; 13 | } 14 | } 15 | .todoDetails { 16 | width: 100%; 17 | display: flex; 18 | flex-direction: row; 19 | align-items: center; 20 | justify-content: space-between; 21 | gap: 1rem; 22 | } 23 | .svgBox { 24 | flex-basis: 25px; 25 | flex-shrink: 0; 26 | height: 25px; 27 | border-radius: 2px; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | padding: 5px; 32 | cursor: pointer; 33 | transition: 0.3s ease background-color; 34 | &:hover { 35 | background-color: var(--grey-2); 36 | } 37 | svg { 38 | width: 100%; 39 | height: 100%; 40 | stroke: white; 41 | display: flex; 42 | align-items: center; 43 | justify-content: center; 44 | } 45 | } 46 | .textsContainer { 47 | min-width: 0; 48 | flex-grow: 1; 49 | } 50 | .texts { 51 | display: flex; 52 | flex-direction: column; 53 | overflow: hidden; 54 | } 55 | .todoText { 56 | word-break: break-all; 57 | font-weight: 500; 58 | font-size: 18px; 59 | color: var(--black-2); 60 | } 61 | .todoDescr { 62 | overflow: hidden; 63 | text-overflow: ellipsis; 64 | white-space: nowrap; 65 | font-size: 14px; 66 | font-weight: 300; 67 | margin-top: -0.2rem; 68 | color: var(--black-2); 69 | } 70 | .timesContainer { 71 | display: flex; 72 | align-items: center; 73 | justify-content: space-between; 74 | } 75 | .time { 76 | display: block; 77 | font-size: 12px; 78 | font-weight: 300; 79 | margin-top: -0.2rem; 80 | color: var(--black-2); 81 | } 82 | .todoActions { 83 | display: flex; 84 | align-items: center; 85 | justify-content: center; 86 | gap: 1rem; 87 | } 88 | .icon { 89 | font-size: 2rem; 90 | padding: 0.5rem; 91 | border-radius: 4px; 92 | background-color: var(--gray-1); 93 | color: var(--black-2); 94 | display: flex; 95 | align-items: center; 96 | justify-content: center; 97 | cursor: pointer; 98 | transition: 0.3s ease background-color; 99 | &:hover { 100 | background-color: var(--gray-2); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/components/AppHeader.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import Button, { SelectButton } from './Button'; 4 | import styles from '../styles/modules/app.module.scss'; 5 | import TodoModal from './TodoModal'; 6 | import { updateSortStatus, updateKeywordStatus } from '../slices/todoSlice'; 7 | 8 | function AppHeader() { 9 | const [modalOpen, setModalOpen] = useState(false); 10 | const initialSortStatus = useSelector((state) => state.todo.sortStatus); 11 | const initialKeywordStatus = useSelector((state) => state.todo.keywordStatus); 12 | 13 | const [sortStatus, setSortStatus] = useState(initialSortStatus); 14 | const [searchKeyword, setSearchKeyword] = useState(initialKeywordStatus); 15 | const dispatch = useDispatch(); 16 | 17 | const updateSort = (e) => { 18 | setSortStatus(e.target.value); 19 | dispatch(updateSortStatus(e.target.value)); 20 | }; 21 | 22 | const updateSearchKeyword = (e) => { 23 | setSearchKeyword(e.target.value); 24 | dispatch(updateKeywordStatus(e.target.value)); 25 | }; 26 | 27 | return ( 28 |
29 |
30 | 33 | 48 |
49 |
50 | 56 |
57 | 58 |
59 | ); 60 | } 61 | 62 | export default AppHeader; 63 | -------------------------------------------------------------------------------- /src/components/AppContent.js: -------------------------------------------------------------------------------- 1 | import { AnimatePresence, motion } from "framer-motion"; 2 | import React from "react"; 3 | import { useSelector } from "react-redux"; 4 | import styles from "../styles/modules/app.module.scss"; 5 | import TodoItem from "./TodoItem"; 6 | 7 | const container = { 8 | hidden: { opacity: 1 }, 9 | visible: { 10 | opacity: 1, 11 | scale: 1, 12 | transition: { 13 | staggerChildren: 0.2, 14 | }, 15 | }, 16 | }; 17 | const child = { 18 | hidden: { y: 20, opacity: 0 }, 19 | visible: { 20 | y: 0, 21 | opacity: 1, 22 | }, 23 | }; 24 | 25 | function AppContent() { 26 | const todoList = useSelector((state) => state.todo.todoList); 27 | const sortStatus = useSelector((state) => state.todo.sortStatus); 28 | const keywordStatus = useSelector((state) => state.todo.keywordStatus); 29 | 30 | const sortedTodoList = [...todoList]; 31 | sortedTodoList.sort((a, b) => { 32 | if (sortStatus === "alphabet_asc") { 33 | return a.title > b.title ? 1 : -1; 34 | } 35 | if (sortStatus === "alphabet_desc") { 36 | return a.title < b.title ? 1 : -1; 37 | } 38 | 39 | if (sortStatus === "created_date_asc") { 40 | return new Date(a.created_time) > new Date(b.created_time) ? 1 : -1; 41 | } 42 | 43 | if (sortStatus === "created_date_desc") { 44 | return new Date(a.created_time) < new Date(b.created_time) ? 1 : -1; 45 | } 46 | 47 | if (sortStatus === "updated_date_asc") { 48 | return new Date(a.updated_time) > new Date(b.updated_time) ? 1 : -1; 49 | } 50 | 51 | if (sortStatus === "updated_date_desc") { 52 | return new Date(a.updated_time) < new Date(b.updated_time) ? 1 : -1; 53 | } 54 | 55 | return a.title > b.title ? 1 : -1; 56 | }); 57 | 58 | const filteredTodoList = sortedTodoList.filter( 59 | (item) => !!item.title.includes(keywordStatus) 60 | ); 61 | 62 | return ( 63 | 69 | 70 | {filteredTodoList && filteredTodoList.length > 0 ? ( 71 | filteredTodoList.map((todo) => ) 72 | ) : ( 73 | 74 | No Notes 75 | 76 | )} 77 | 78 | 79 | ); 80 | } 81 | 82 | export default AppContent; 83 | -------------------------------------------------------------------------------- /src/slices/todoSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const getInitialTodo = () => { 4 | // getting todo list 5 | const localTodoList = window.localStorage.getItem('todoList'); 6 | // if todo list is not empty 7 | if (localTodoList) { 8 | return JSON.parse(localTodoList); 9 | } 10 | window.localStorage.setItem('todoList', []); 11 | return []; 12 | }; 13 | 14 | const initialValue = { 15 | sortStatus: 'created_date_desc', 16 | keywordStatus: '', 17 | todoList: getInitialTodo(), 18 | }; 19 | 20 | export const todoSlice = createSlice({ 21 | name: 'todo', 22 | initialState: initialValue, 23 | reducers: { 24 | addTodo: (state, action) => { 25 | state.todoList.push(action.payload); 26 | const todoList = window.localStorage.getItem('todoList'); 27 | if (todoList) { 28 | const todoListArr = JSON.parse(todoList); 29 | todoListArr.push({ 30 | ...action.payload, 31 | }); 32 | window.localStorage.setItem('todoList', JSON.stringify(todoListArr)); 33 | } else { 34 | window.localStorage.setItem( 35 | 'todoList', 36 | JSON.stringify([ 37 | { 38 | ...action.payload, 39 | }, 40 | ]) 41 | ); 42 | } 43 | }, 44 | updateTodo: (state, action) => { 45 | const todoList = window.localStorage.getItem('todoList'); 46 | if (todoList) { 47 | const todoListArr = JSON.parse(todoList); 48 | todoListArr.forEach((todo) => { 49 | if (todo.id === action.payload.id) { 50 | todo.description = action.payload.description; 51 | todo.title = action.payload.title; 52 | todo.updated_time = action.payload.updated_time; 53 | } 54 | }); 55 | window.localStorage.setItem('todoList', JSON.stringify(todoListArr)); 56 | state.todoList = [...todoListArr]; 57 | } 58 | }, 59 | deleteTodo: (state, action) => { 60 | const todoList = window.localStorage.getItem('todoList'); 61 | if (todoList) { 62 | const todoListArr = JSON.parse(todoList); 63 | todoListArr.forEach((todo, index) => { 64 | if (todo.id === action.payload) { 65 | todoListArr.splice(index, 1); 66 | } 67 | }); 68 | window.localStorage.setItem('todoList', JSON.stringify(todoListArr)); 69 | state.todoList = todoListArr; 70 | } 71 | }, 72 | updateSortStatus: (state, action) => { 73 | state.sortStatus = action.payload; 74 | }, 75 | updateKeywordStatus: (state, action) => { 76 | state.keywordStatus = action.payload; 77 | }, 78 | }, 79 | }); 80 | 81 | export const { 82 | addTodo, 83 | updateTodo, 84 | deleteTodo, 85 | updateSortStatus, 86 | updateKeywordStatus, 87 | } = todoSlice.actions; 88 | export default todoSlice.reducer; 89 | -------------------------------------------------------------------------------- /src/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | import { motion } from "framer-motion"; 3 | import toast from "react-hot-toast"; 4 | import React, { useEffect, useState } from "react"; 5 | import { MdDelete, MdEdit } from "react-icons/md"; 6 | import { useDispatch } from "react-redux"; 7 | import { deleteTodo, updateTodo } from "../slices/todoSlice"; 8 | import styles from "../styles/modules/todoItem.module.scss"; 9 | import { getClasses } from "../utils/getClasses"; 10 | import CheckButton from "./CheckButton"; 11 | import TodoModal from "./TodoModal"; 12 | 13 | const child = { 14 | hidden: { y: 20, opacity: 0 }, 15 | visible: { 16 | y: 0, 17 | opacity: 1, 18 | }, 19 | }; 20 | 21 | function TodoItem({ todo }) { 22 | const dispatch = useDispatch(); 23 | const [checked, setChecked] = useState(false); 24 | const [updateModalOpen, setUpdateModalOpen] = useState(false); 25 | 26 | useEffect(() => { 27 | if (todo.status === "complete") { 28 | setChecked(true); 29 | } else { 30 | setChecked(false); 31 | } 32 | }, [todo.status]); 33 | 34 | const handleCheck = () => { 35 | setChecked(!checked); 36 | dispatch( 37 | updateTodo({ ...todo, status: checked ? "incomplete" : "complete" }) 38 | ); 39 | }; 40 | 41 | const handleDelete = () => { 42 | dispatch(deleteTodo(todo.id)); 43 | toast.success("Todo Deleted Successfully"); 44 | }; 45 | 46 | const handleUpdate = () => { 47 | setUpdateModalOpen(true); 48 | }; 49 | 50 | return ( 51 | <> 52 | 53 |
54 | 55 |
56 |
57 |

{todo.title}

58 |

{todo.description}

59 |
60 |

61 | Created Date:{" "} 62 | {format(new Date(todo.created_time), "p, MM/dd/yyyy")} 63 |

64 |

65 | Updated Date:{" "} 66 | {format(new Date(todo.updated_time), "p, MM/dd/yyyy")} 67 |

68 |
69 |
70 |
71 |
72 |
handleDelete()} 75 | onKeyDown={() => handleDelete()} 76 | tabIndex={0} 77 | role="button" 78 | > 79 | 80 |
81 |
handleUpdate()} 84 | onKeyDown={() => handleUpdate()} 85 | tabIndex={0} 86 | role="button" 87 | > 88 | 89 |
90 |
91 |
92 |
93 | 99 | 100 | ); 101 | } 102 | 103 | export default TodoItem; 104 | -------------------------------------------------------------------------------- /src/components/TodoModal.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { v4 as uuid } from "uuid"; 3 | import { MdOutlineClose } from "react-icons/md"; 4 | import { useDispatch } from "react-redux"; 5 | import { AnimatePresence, motion } from "framer-motion"; 6 | import toast from "react-hot-toast"; 7 | import { addTodo, updateTodo } from "../slices/todoSlice"; 8 | import styles from "../styles/modules/modal.module.scss"; 9 | import Button from "./Button"; 10 | 11 | const dropIn = { 12 | hidden: { 13 | opacity: 0, 14 | transform: "scale(0.9)", 15 | }, 16 | visible: { 17 | transform: "scale(1)", 18 | opacity: 1, 19 | transition: { 20 | duration: 0.1, 21 | type: "spring", 22 | damping: 25, 23 | stiffness: 500, 24 | }, 25 | }, 26 | exit: { 27 | transform: "scale(0.9)", 28 | opacity: 0, 29 | }, 30 | }; 31 | 32 | function TodoModal({ type, modalOpen, setModalOpen, todo }) { 33 | const dispatch = useDispatch(); 34 | const [title, setTitle] = useState(""); 35 | const [description, setDescription] = useState(""); 36 | 37 | useEffect(() => { 38 | if (type === "update" && todo) { 39 | setTitle(todo.title); 40 | setDescription(todo.description); 41 | } else { 42 | setTitle(""); 43 | setDescription(""); 44 | } 45 | }, [type, todo, modalOpen]); 46 | 47 | const handleSubmit = (e) => { 48 | e.preventDefault(); 49 | if (title === "") { 50 | toast.error("Please enter a title"); 51 | return; 52 | } 53 | 54 | if (description === "") { 55 | toast.error("Please enter a description"); 56 | return; 57 | } 58 | 59 | if (title && description) { 60 | if (type === "add") { 61 | dispatch( 62 | addTodo({ 63 | id: uuid(), 64 | title, 65 | description, 66 | created_time: new Date().toLocaleString(), 67 | updated_time: new Date().toLocaleString(), 68 | }) 69 | ); 70 | toast.success("Task added successfully"); 71 | } 72 | if (type === "update") { 73 | if (todo.title !== title || todo.description !== description) { 74 | dispatch( 75 | updateTodo({ 76 | ...todo, 77 | title, 78 | description, 79 | updated_time: new Date().toLocaleString(), 80 | }) 81 | ); 82 | toast.success("Task Updated successfully"); 83 | } else { 84 | toast.error("No changes made"); 85 | return; 86 | } 87 | } 88 | setModalOpen(false); 89 | } 90 | }; 91 | 92 | return ( 93 | 94 | {modalOpen && ( 95 | 101 | 108 | setModalOpen(false)} 111 | onClick={() => setModalOpen(false)} 112 | role="button" 113 | tabIndex={0} 114 | // animation 115 | initial={{ top: 40, opacity: 0 }} 116 | animate={{ top: -10, opacity: 1 }} 117 | exit={{ top: 40, opacity: 0 }} 118 | > 119 | 120 | 121 | 122 |
handleSubmit(e)}> 123 |

124 | {type === "add" ? "Add" : "Update"} Note 125 |

126 | 135 |