├── 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 |
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 |
152 |
153 |
154 | )}
155 |
156 | );
157 | }
158 |
159 | export default TodoModal;
160 |
--------------------------------------------------------------------------------