├── README.md ├── public ├── To-Do.webm ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── To-Do List _ All tasks.png ├── manifest.json └── index.html ├── src ├── assets │ ├── avatar-1.jpg │ ├── x.svg │ ├── check.svg │ ├── menu.svg │ ├── search.svg │ ├── arrow.svg │ ├── bell.svg │ ├── edit.svg │ ├── view-1.svg │ ├── options.svg │ ├── sort-alfa-down.svg │ ├── star-line.svg │ ├── view-2.svg │ ├── trash.svg │ ├── date.svg │ └── sort-number-down.svg ├── components │ ├── Footer.tsx │ ├── Routes │ │ ├── Home.tsx │ │ ├── TodaysTasks.tsx │ │ ├── DoneTasks.tsx │ │ ├── ImportantTasks.tsx │ │ ├── SearchResults.tsx │ │ ├── TaskOnly.tsx │ │ └── Directory.tsx │ ├── hooks │ │ ├── useDate.tsx │ │ ├── useDescriptionTitle.tsx │ │ ├── useCompletedTasks.tsx │ │ ├── useSearchQuery.tsx │ │ ├── useTodayTasks.tsx │ │ ├── useVisibility.tsx │ │ ├── useScreenMedia.tsx │ │ └── useSortTasks.tsx │ ├── Utilities │ │ ├── BtnAddTask.tsx │ │ ├── ModalConfirm.tsx │ │ ├── LayoutMenus.tsx │ │ ├── Modal.tsx │ │ ├── ModalDirectory.tsx │ │ ├── LayoutRoutes.tsx │ │ └── ModalTask.tsx │ ├── TasksSection │ │ ├── TaskItem │ │ │ ├── BtnMarkAsImportant.tsx │ │ │ ├── ActionsTaskItem.tsx │ │ │ ├── BtnDeleteTask.tsx │ │ │ ├── InfosTask.tsx │ │ │ ├── TaskItem.tsx │ │ │ ├── BtnEditTask.tsx │ │ │ └── BtnToggleCompleted.tsx │ │ ├── TasksSection.tsx │ │ ├── ButtonsSort.tsx │ │ ├── HeaderTasks.tsx │ │ ├── Notification.tsx │ │ └── SearchField.tsx │ ├── AccountSection │ │ ├── DeleteTasks.tsx │ │ ├── DarkMode.tsx │ │ ├── AccountData.tsx │ │ └── TasksDone.tsx │ └── Menu │ │ ├── Directories │ │ ├── Directories.tsx │ │ ├── ContentDirectories.tsx │ │ └── ItemDirectory.tsx │ │ ├── NavLinks.tsx │ │ └── Menu.tsx ├── interfaces.tsx ├── custom.d.ts ├── store │ ├── hooks.tsx │ ├── Modal.store.tsx │ ├── index.tsx │ ├── Menu.store.tsx │ └── Tasks.store.tsx ├── index.tsx ├── App.tsx └── index.css ├── postcss.config.js ├── tailwind.config.js ├── tsconfig.json ├── package.json └── .gitignore /README.md: -------------------------------------------------------------------------------- 1 | "# react_todo_app" 2 | -------------------------------------------------------------------------------- /public/To-Do.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asrafaliii/react_todo_app/HEAD/public/To-Do.webm -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asrafaliii/react_todo_app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asrafaliii/react_todo_app/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asrafaliii/react_todo_app/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/assets/avatar-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asrafaliii/react_todo_app/HEAD/src/assets/avatar-1.jpg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/To-Do List _ All tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asrafaliii/react_todo_app/HEAD/public/To-Do List _ All tasks.png -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer: React.FC = () => { 4 | return ; 5 | }; 6 | 7 | export default Footer; 8 | -------------------------------------------------------------------------------- /src/interfaces.tsx: -------------------------------------------------------------------------------- 1 | export interface Task { 2 | title: string; 3 | dir: string; 4 | description: string; 5 | date: string; 6 | completed: boolean; 7 | important: boolean; 8 | id: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/assets/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | import React = require("react"); 3 | export const ReactComponent: React.FC>; 4 | const src: string; 5 | export default src; 6 | } 7 | 8 | declare module "*.jpg" { 9 | const value: any; 10 | export = value; 11 | } 12 | -------------------------------------------------------------------------------- /src/store/hooks.tsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector, TypedUseSelectorHook } from "react-redux"; 2 | // @ts-ignore 3 | import type { RootState, AppDispatch } from "./index.tsx"; 4 | 5 | export const useAppDispatch: () => AppDispatch = useDispatch; 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /src/assets/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | slate: { 8 | 800: "#141E33", 9 | }, 10 | }, 11 | screens: { 12 | "2xl": "1736px", 13 | }, 14 | }, 15 | }, 16 | plugins: [require("@tailwindcss/line-clamp")], 17 | darkMode: "class", 18 | }; 19 | -------------------------------------------------------------------------------- /src/assets/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/view-1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/options.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Routes/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import LayoutRoutes from "../Utilities/LayoutRoutes"; 3 | import { useAppSelector } from "../../store/hooks"; 4 | import useDescriptionTitle from "../hooks/useDescriptionTitle"; 5 | 6 | const Home: React.FC = () => { 7 | const tasks = useAppSelector((state) => state.tasks.tasks); 8 | 9 | useDescriptionTitle("Organize your tasks", "All tasks"); 10 | return ; 11 | }; 12 | 13 | export default Home; 14 | -------------------------------------------------------------------------------- /src/components/hooks/useDate.tsx: -------------------------------------------------------------------------------- 1 | const useDate = (date: string): string => { 2 | const fullDate: Date = new Date(date.replaceAll("-", "/")); 3 | const year: number = fullDate.getFullYear(); 4 | const month: number = fullDate.getMonth() + 1; 5 | const day: number = fullDate.getDate(); 6 | 7 | const dateFormated: string = 8 | month.toString().padStart(2, "0") + 9 | "/" + 10 | day.toString().padStart(2, "0") + 11 | "/" + 12 | year; 13 | 14 | return dateFormated; 15 | }; 16 | 17 | export default useDate; 18 | -------------------------------------------------------------------------------- /src/components/hooks/useDescriptionTitle.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | const useDescriptionTitle = (description: string, title: string): void => { 4 | useEffect(() => { 5 | const metaDescription = document.querySelector('meta[name="description"]')!; 6 | metaDescription.setAttribute("content", description); 7 | 8 | const titleElement = document.querySelector("title")!; 9 | titleElement.innerText = "To-Do List | " + title; 10 | }, [description, title]); 11 | }; 12 | 13 | export default useDescriptionTitle; 14 | -------------------------------------------------------------------------------- /src/components/Routes/TodaysTasks.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useDescriptionTitle from "../hooks/useDescriptionTitle"; 3 | import useTodayTasks from "../hooks/useTodayTasks"; 4 | import LayoutRoutes from "../Utilities/LayoutRoutes"; 5 | 6 | const TodaysTasks: React.FC = () => { 7 | const todaysTasks = useTodayTasks(); 8 | 9 | useDescriptionTitle("Today's tasks", "Today's tasks"); 10 | 11 | return ( 12 | 13 | ); 14 | }; 15 | 16 | export default TodaysTasks; 17 | -------------------------------------------------------------------------------- /src/assets/sort-alfa-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/store/Modal.store.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | modalCreateTaskOpen: false, 5 | }; 6 | 7 | const modalSlice = createSlice({ 8 | name: "modal", 9 | initialState: initialState, 10 | reducers: { 11 | openModalCreateTask(state) { 12 | state.modalCreateTaskOpen = true; 13 | }, 14 | closeModalCreateTask(state) { 15 | state.modalCreateTaskOpen = false; 16 | }, 17 | }, 18 | }); 19 | 20 | export const modalActions = modalSlice.actions; 21 | export default modalSlice.reducer; 22 | -------------------------------------------------------------------------------- /src/assets/star-line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { Provider } from "react-redux"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import store from "./store/index"; 6 | import App from "./App"; 7 | import "./index.css"; 8 | 9 | const root = ReactDOM.createRoot( 10 | document.getElementById("root") as HTMLElement 11 | ); 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /src/store/index.tsx: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import tasksReducer, { tasksMiddleware } from "./Tasks.store"; 3 | import modalReducer from "./Modal.store"; 4 | import menuReducer from "./Menu.store"; 5 | 6 | const store = configureStore({ 7 | reducer: { tasks: tasksReducer, modal: modalReducer, menu: menuReducer }, 8 | middleware: (getDefaultMiddleware: any) => 9 | getDefaultMiddleware().concat(tasksMiddleware), 10 | }); 11 | 12 | export type RootState = ReturnType; 13 | export type AddDispatch = typeof store.dispatch; 14 | export default store; 15 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Utilities/BtnAddTask.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAppDispatch } from "../../store/hooks"; 3 | import { modalActions } from "../../store/Modal.store"; 4 | 5 | const BtnAddTask: React.FC<{ className?: string }> = ({ className }) => { 6 | const dispatch = useAppDispatch(); 7 | 8 | const onOpenModal = () => { 9 | dispatch(modalActions.openModalCreateTask()); 10 | }; 11 | return ( 12 | <> 13 | 16 | 17 | ); 18 | }; 19 | 20 | export default BtnAddTask; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src", "src/custom.d.ts", "src/components"] 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/view-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/hooks/useCompletedTasks.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Task } from "../../interfaces"; 3 | 4 | interface Props { 5 | tasks: Task[]; 6 | done: boolean; 7 | } 8 | 9 | const useCompletedTasks = (props: Props): { tasks: Task[] } => { 10 | const [tasks, setTasks] = useState([]); 11 | 12 | useEffect(() => { 13 | const filteredTasks: Task[] = props.tasks.filter((task: Task) => { 14 | if (props.done) { 15 | return task.completed; 16 | } else { 17 | return !task.completed; 18 | } 19 | }); 20 | setTasks(filteredTasks); 21 | }, [props.tasks, props.done]); 22 | 23 | return { tasks }; 24 | }; 25 | 26 | export default useCompletedTasks; 27 | -------------------------------------------------------------------------------- /src/assets/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/date.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Routes/DoneTasks.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAppSelector } from "../../store/hooks"; 3 | import useCompletedTasks from "../hooks/useCompletedTasks"; 4 | import useDescriptionTitle from "../hooks/useDescriptionTitle"; 5 | import LayoutRoutes from "../Utilities/LayoutRoutes"; 6 | 7 | const DoneTasks: React.FC<{ done: boolean; title: string }> = ({ 8 | done, 9 | title, 10 | }) => { 11 | const tasks = useAppSelector((state) => state.tasks.tasks); 12 | 13 | const { tasks: tasksFiltered } = useCompletedTasks({ tasks, done }); 14 | 15 | useDescriptionTitle("All tasks done", title); 16 | 17 | return ; 18 | }; 19 | 20 | export default DoneTasks; 21 | -------------------------------------------------------------------------------- /src/store/Menu.store.tsx: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | const initialState = { 4 | menuHeaderOpened: false, 5 | menuAccountOpened: false, 6 | }; 7 | 8 | const menusSlice = createSlice({ 9 | name: "modal", 10 | initialState: initialState, 11 | reducers: { 12 | openMenuHeader(state) { 13 | state.menuHeaderOpened = true; 14 | }, 15 | closeMenuHeader(state) { 16 | state.menuHeaderOpened = false; 17 | }, 18 | openMenuAccount(state) { 19 | state.menuAccountOpened = true; 20 | }, 21 | closeMenuAccount(state) { 22 | state.menuAccountOpened = false; 23 | }, 24 | }, 25 | }); 26 | 27 | export const menusActions = menusSlice.actions; 28 | export default menusSlice.reducer; 29 | -------------------------------------------------------------------------------- /src/components/Utilities/ModalConfirm.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Modal from "./Modal"; 3 | 4 | const ModalConfirm: React.FC<{ 5 | onConfirm: () => void; 6 | onClose: () => void; 7 | text: string; 8 | }> = ({ onConfirm, onClose, text }) => { 9 | const confirmAndCloseModal = () => { 10 | onConfirm(); 11 | onClose(); 12 | }; 13 | return ( 14 | 15 |

{text}

16 |
17 | 18 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default ModalConfirm; 27 | -------------------------------------------------------------------------------- /src/assets/sort-number-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/hooks/useSearchQuery.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Task } from "../../interfaces"; 3 | import { useAppSelector } from "../../store/hooks"; 4 | 5 | const useSearchQuery = (searchQuery: string) => { 6 | const tasks = useAppSelector((state) => state.tasks.tasks); 7 | 8 | const [matchedTasks, setMatchedTasks] = useState([]); 9 | 10 | useEffect(() => { 11 | const filteredTasks = tasks.filter((task: Task) => { 12 | return task.title.toLowerCase().includes(searchQuery.toLowerCase()); 13 | }); 14 | if (searchQuery.trim().length) { 15 | setMatchedTasks(filteredTasks); 16 | } else { 17 | setMatchedTasks([]); 18 | } 19 | }, [searchQuery, tasks]); 20 | 21 | return matchedTasks; 22 | }; 23 | 24 | export default useSearchQuery; 25 | -------------------------------------------------------------------------------- /src/components/Routes/ImportantTasks.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Task } from "../../interfaces"; 3 | import { useAppSelector } from "../../store/hooks"; 4 | import useDescriptionTitle from "../hooks/useDescriptionTitle"; 5 | import LayoutRoutes from "../Utilities/LayoutRoutes"; 6 | 7 | const ImportantTasks: React.FC = () => { 8 | const tasks = useAppSelector((state) => state.tasks.tasks); 9 | const [importantTasks, setImportantTasks] = useState([]); 10 | 11 | useEffect(() => { 12 | const filteredTasks: Task[] = tasks.filter((task: Task) => task.important); 13 | setImportantTasks(filteredTasks); 14 | }, [tasks]); 15 | 16 | useDescriptionTitle("Tasks marked as important", "Important tasks"); 17 | 18 | return ( 19 | 20 | ); 21 | }; 22 | 23 | export default ImportantTasks; 24 | -------------------------------------------------------------------------------- /src/components/hooks/useTodayTasks.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Task } from "../../interfaces"; 3 | import { useAppSelector } from "../../store/hooks"; 4 | 5 | const useTodayTasks = (): Task[] => { 6 | const tasks = useAppSelector((state) => state.tasks.tasks); 7 | const [todaysTasks, setTodaysTasks] = useState([]); 8 | 9 | const date: Date = new Date(); 10 | const year: number = date.getFullYear(); 11 | const month: number = date.getMonth() + 1; 12 | const day: number = date.getDate(); 13 | 14 | const dateTimeFormat = `${year}-${month.toString().padStart(2, "0")}-${day 15 | .toString() 16 | .padStart(2, "0")}`; 17 | 18 | useEffect(() => { 19 | let filteredTasks: Task[] = tasks.filter( 20 | (task: Task) => task.date === dateTimeFormat 21 | ); 22 | setTodaysTasks(filteredTasks); 23 | }, [dateTimeFormat, tasks]); 24 | return todaysTasks; 25 | }; 26 | 27 | export default useTodayTasks; 28 | -------------------------------------------------------------------------------- /src/components/Utilities/LayoutMenus.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import useScreenMedia from "../hooks/useScreenMedia"; 3 | 4 | const LayoutMenus: React.FC<{ 5 | menuOpen: boolean; 6 | children: ReactNode; 7 | closeMenuHandler: () => void; 8 | className?: string; 9 | }> = ({ menuOpen, children, closeMenuHandler, className }) => { 10 | const mediaQueries = useScreenMedia(); 11 | 12 | return ( 13 | <> 14 |
19 | {children} 20 |
21 | {menuOpen && !mediaQueries.xl && ( 22 |
26 | )} 27 | 28 | ); 29 | }; 30 | 31 | export default LayoutMenus; 32 | -------------------------------------------------------------------------------- /src/components/TasksSection/TaskItem/BtnMarkAsImportant.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAppDispatch } from "../../../store/hooks"; 3 | import { tasksActions } from "../../../store/Tasks.store"; 4 | import { ReactComponent as StarLine } from "../../../assets/star-line.svg"; 5 | 6 | const BtnMarkAsImportant: React.FC<{ 7 | taskId: string; 8 | taskImportant: boolean; 9 | }> = ({ taskId, taskImportant }) => { 10 | const dispatch = useAppDispatch(); 11 | 12 | const markAsImportantHandler = () => { 13 | dispatch(tasksActions.markAsImportant(taskId)); 14 | }; 15 | 16 | return ( 17 | 28 | ); 29 | }; 30 | 31 | export default React.memo(BtnMarkAsImportant); 32 | -------------------------------------------------------------------------------- /src/components/AccountSection/DeleteTasks.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useAppDispatch } from "../../store/hooks"; 3 | import { tasksActions } from "../../store/Tasks.store"; 4 | import ModalConfirm from "../Utilities/ModalConfirm"; 5 | 6 | const DeleteTasks: React.FC = () => { 7 | const dispatch = useAppDispatch(); 8 | 9 | const [showModal, setIsModalShown] = useState(false); 10 | 11 | const deleteAllDataHandler = () => { 12 | dispatch(tasksActions.deleteAllData()); 13 | }; 14 | 15 | return ( 16 | <> 17 | {showModal && ( 18 | setIsModalShown(false)} 20 | text="All data will be deleted permanently." 21 | onConfirm={deleteAllDataHandler} 22 | /> 23 | )} 24 | 30 | 31 | ); 32 | }; 33 | 34 | export default React.memo(DeleteTasks); 35 | -------------------------------------------------------------------------------- /src/components/Routes/SearchResults.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useNavigate, useSearchParams } from "react-router-dom"; 3 | import useDescriptionTitle from "../hooks/useDescriptionTitle"; 4 | import useSearchQuery from "../hooks/useSearchQuery"; 5 | import LayoutRoutes from "../Utilities/LayoutRoutes"; 6 | 7 | const SearchResults: React.FC = () => { 8 | const [searchParams] = useSearchParams(); 9 | const navigate = useNavigate(); 10 | 11 | const [currQueryParam, setCurrQueryParam] = useState(""); 12 | 13 | useEffect(() => { 14 | const query = searchParams.get("q"); 15 | if (!query) { 16 | // se "q" for igual a '' ou "q" não existir 17 | navigate("/"); 18 | } else { 19 | setCurrQueryParam(query); 20 | } 21 | }, [navigate, searchParams]); 22 | 23 | const matchedTasks = useSearchQuery(currQueryParam); 24 | 25 | const title = `Results for "${currQueryParam}"`; 26 | 27 | useDescriptionTitle(title, title); 28 | 29 | return ; 30 | }; 31 | 32 | export default SearchResults; 33 | -------------------------------------------------------------------------------- /src/components/TasksSection/TaskItem/ActionsTaskItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Task } from "../../../interfaces"; 3 | import BtnEditTask from "./BtnEditTask"; 4 | import BtnMarkAsImportant from "./BtnMarkAsImportant"; 5 | import BtnDeleteTask from "./BtnDeleteTask"; 6 | import BtnToggleCompleted from "./BtnToggleCompleted"; 7 | 8 | const ActionsTaskItem: React.FC<{ task: Task; isListInView1: boolean }> = ({ 9 | task, 10 | isListInView1, 11 | }) => { 12 | return ( 13 | <> 14 |
19 | 24 | 25 | 26 | 27 |
28 | 29 | ); 30 | }; 31 | 32 | export default ActionsTaskItem; 33 | -------------------------------------------------------------------------------- /src/components/Routes/TaskOnly.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useNavigate, useParams } from "react-router-dom"; 3 | import { Task } from "../../interfaces"; 4 | import { useAppSelector } from "../../store/hooks"; 5 | import useDescriptionTitle from "../hooks/useDescriptionTitle"; 6 | import LayoutRoutes from "../Utilities/LayoutRoutes"; 7 | 8 | const TaskOnly: React.FC = () => { 9 | const params = useParams(); 10 | const navigate = useNavigate(); 11 | 12 | const tasks = useAppSelector((store) => store.tasks.tasks); 13 | 14 | const [matchedTask, setMatchedTask] = useState([]); 15 | 16 | useEffect(() => { 17 | const taskId = params.taskId; 18 | const filteredTask = tasks.filter((task: Task) => taskId === task.id); 19 | if (!filteredTask.length) { 20 | navigate("/"); 21 | } 22 | setMatchedTask(filteredTask); 23 | }, [navigate, params.taskId, tasks]); 24 | 25 | const title = matchedTask.length ? matchedTask[0].title : ""; 26 | 27 | useDescriptionTitle(`Searching for ${title}`, "Task " + title); 28 | 29 | return ; 30 | }; 31 | 32 | export default TaskOnly; 33 | -------------------------------------------------------------------------------- /src/components/Menu/Directories/Directories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { ReactComponent as Arrow } from "../../../assets/arrow.svg"; 3 | import ContentDirectories from "./ContentDirectories"; 4 | 5 | const Directories: React.FC<{ classActive: string }> = ({ classActive }) => { 6 | const [isDirectoriesOpen, setIsDirectoriesOpen] = useState(true); 7 | 8 | const toggleDirectoriesOpen = () => { 9 | setIsDirectoriesOpen((prevState) => !prevState); 10 | }; 11 | 12 | return ( 13 |
14 | 27 |
28 | 29 |
30 |
31 | ); 32 | }; 33 | 34 | export default Directories; 35 | -------------------------------------------------------------------------------- /src/components/TasksSection/TaskItem/BtnDeleteTask.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useAppDispatch } from "../../../store/hooks"; 3 | import { tasksActions } from "../../../store/Tasks.store"; 4 | import ModalConfirm from "../../Utilities/ModalConfirm"; 5 | import { ReactComponent as Trash } from "../../../assets/trash.svg"; 6 | 7 | const BtnDeleteTask: React.FC<{ taskId: string }> = ({ taskId }) => { 8 | const [showModal, setIsModalShown] = useState(false); 9 | const dispatch = useAppDispatch(); 10 | 11 | const removeTaskHandler = () => { 12 | dispatch(tasksActions.removeTask(taskId)); 13 | }; 14 | return ( 15 | <> 16 | {showModal && ( 17 | setIsModalShown(false)} 19 | text="This task will be deleted permanently." 20 | onConfirm={removeTaskHandler} 21 | /> 22 | )} 23 | 30 | 31 | ); 32 | }; 33 | 34 | export default React.memo(BtnDeleteTask); 35 | -------------------------------------------------------------------------------- /src/components/Menu/NavLinks.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink, useLocation } from "react-router-dom"; 3 | 4 | const links = [ 5 | { 6 | name: "Today's tasks", 7 | path: "/today", 8 | }, 9 | { 10 | name: "All tasks", 11 | path: "/", 12 | }, 13 | { 14 | name: "Important tasks", 15 | path: "/important", 16 | }, 17 | { 18 | name: "Completed tasks", 19 | path: "/completed", 20 | }, 21 | { 22 | name: "Uncompleted tasks", 23 | path: "/uncompleted", 24 | }, 25 | ]; 26 | 27 | const NavLinks: React.FC<{ classActive: string }> = ({ classActive }) => { 28 | const route = useLocation(); 29 | const currentPath = route.pathname; 30 | return ( 31 | 47 | ); 48 | }; 49 | 50 | export default NavLinks; 51 | -------------------------------------------------------------------------------- /src/components/TasksSection/TaskItem/InfosTask.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Task } from "../../../interfaces"; 3 | import { ReactComponent as Calendar } from "../../../assets/date.svg"; 4 | import useDate from "../../hooks/useDate"; 5 | 6 | const InfosTask: React.FC<{ task: Task; isListInView1: boolean }> = ({ 7 | task, 8 | isListInView1, 9 | }) => { 10 | const dateFormated = useDate(task.date); 11 | 12 | return ( 13 |
14 |
19 | 20 | {task.title} 21 | 22 |
23 |

29 | {task.description} 30 |

31 | 34 |
35 | ); 36 | }; 37 | 38 | export default InfosTask; 39 | -------------------------------------------------------------------------------- /src/components/hooks/useVisibility.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const useVisibility = (elements: HTMLElement[], fnClose?: () => void) => { 4 | const [elementIsVisible, setElementIsVisible] = useState(false); 5 | 6 | useEffect(() => { 7 | const checkClick = (e: MouseEvent) => { 8 | if (!elements) return; 9 | 10 | const clickedOutsideElement = elements.every((element) => { 11 | if (!element) return false; 12 | if ( 13 | e.target !== element && 14 | !element.contains(e.target as HTMLElement) 15 | ) { 16 | return true; 17 | } 18 | return false; 19 | }); 20 | 21 | if (clickedOutsideElement) { 22 | setElementIsVisible(false); 23 | if (fnClose) fnClose(); 24 | } 25 | }; 26 | 27 | document.addEventListener("click", checkClick); 28 | return () => { 29 | document.removeEventListener("click", checkClick); 30 | }; 31 | }, [elements, fnClose]); 32 | 33 | const closeElement = () => { 34 | setElementIsVisible(false); 35 | }; 36 | 37 | const showElement = () => { 38 | setElementIsVisible(true); 39 | }; 40 | 41 | return { elementIsVisible, closeElement, showElement }; 42 | }; 43 | 44 | export default useVisibility; 45 | -------------------------------------------------------------------------------- /src/components/Routes/Directory.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useNavigate, useParams } from "react-router-dom"; 3 | import { Task } from "../../interfaces"; 4 | import { useAppSelector } from "../../store/hooks"; 5 | import useDescriptionTitle from "../hooks/useDescriptionTitle"; 6 | import LayoutRoutes from "../Utilities/LayoutRoutes"; 7 | 8 | const Directory: React.FC = () => { 9 | const tasks = useAppSelector((state) => state.tasks.tasks); 10 | const directories = useAppSelector((state) => state.tasks.directories); 11 | const params = useParams(); 12 | const navigate = useNavigate(); 13 | 14 | useDescriptionTitle( 15 | `Tasks in "${params.dir}"`, 16 | params.dir ? params.dir + " directory" : "" 17 | ); 18 | 19 | const [tasksInCurrentDirectory, setTasksInCurrentDirectory] = useState< 20 | Task[] 21 | >([]); 22 | 23 | useEffect(() => { 24 | const dirExists = directories.includes(params.dir); 25 | if (!dirExists) { 26 | navigate("/"); 27 | } 28 | const tasksFiltered = tasks.filter((task: Task) => task.dir === params.dir); 29 | setTasksInCurrentDirectory(tasksFiltered); 30 | }, [directories, navigate, params.dir, tasks]); 31 | 32 | return ( 33 | 37 | ); 38 | }; 39 | 40 | export default Directory; 41 | -------------------------------------------------------------------------------- /src/components/TasksSection/TaskItem/TaskItem.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Task } from "../../../interfaces"; 3 | import { Link } from "react-router-dom"; 4 | import InfosTask from "./InfosTask"; 5 | import ActionsTaskItem from "./ActionsTaskItem"; 6 | 7 | const TaskItem: React.FC<{ isListInView1: boolean; task: Task }> = ({ 8 | isListInView1, 9 | task, 10 | }) => { 11 | return ( 12 | <> 13 |
  • 14 | 19 | {task.dir} 20 | 21 |
    26 | 27 | 28 |
    29 |
  • 30 | 31 | ); 32 | }; 33 | 34 | export default React.memo(TaskItem); 35 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAppDispatch, useAppSelector } from "../../store/hooks"; 3 | import { menusActions } from "../../store/Menu.store"; 4 | import BtnAddTask from "../Utilities/BtnAddTask"; 5 | import Directories from "./Directories/Directories"; 6 | import NavLinks from "./NavLinks"; 7 | import LayoutMenus from "../Utilities/LayoutMenus"; 8 | 9 | const classLinkActive = 10 | "text-rose-600 bg-violet-100 border-r-4 border-rose-500 dark:bg-slate-700/[.2] dark:text-slate-200 dark:border-slate-200"; 11 | 12 | const Menu: React.FC = () => { 13 | const menuOpen = useAppSelector((state) => state.menu.menuHeaderOpened); 14 | const dispatch = useAppDispatch(); 15 | 16 | const closeMenuHandler = () => { 17 | dispatch(menusActions.closeMenuHeader()); 18 | }; 19 | return ( 20 | 25 |
    26 |

    27 | To-do list 28 |

    29 | 30 | 31 | 32 |
    33 |
    34 | ); 35 | }; 36 | 37 | export default Menu; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.9.1", 7 | "@tailwindcss/line-clamp": "^0.4.2", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "@types/jest": "^29.2.4", 12 | "@types/node": "^18.11.17", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-redux": "^8.0.5", 16 | "react-router-dom": "^6.5.0", 17 | "react-scripts": "5.0.1", 18 | "typescript": "^4.9.4", 19 | "web-vitals": "^2.1.4" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "devDependencies": { 46 | "@types/react": "^18.0.26", 47 | "@types/react-dom": "^18.0.9", 48 | "autoprefixer": "^10.4.13", 49 | "postcss": "^8.4.20", 50 | "tailwindcss": "^3.2.4" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import AccountData from "./components/AccountSection/AccountData"; 3 | import Footer from "./components/Footer"; 4 | import Menu from "./components/Menu/Menu"; 5 | import TasksSection from "./components/TasksSection/TasksSection"; 6 | import ModalCreateTask from "./components/Utilities/ModalTask"; 7 | import { Task } from "./interfaces"; 8 | import { useAppDispatch, useAppSelector } from "./store/hooks"; 9 | import { modalActions } from "./store/Modal.store"; 10 | import { tasksActions } from "./store/Tasks.store"; 11 | 12 | const App: React.FC = () => { 13 | const modal = useAppSelector((state) => state.modal); 14 | 15 | const dispatch = useAppDispatch(); 16 | 17 | const closeModalCreateTask = () => { 18 | dispatch(modalActions.closeModalCreateTask()); 19 | }; 20 | 21 | const createNewTaskHandler = (task: Task) => { 22 | dispatch(tasksActions.addNewTask(task)); 23 | }; 24 | 25 | return ( 26 |
    27 | {modal.modalCreateTaskOpen && ( 28 | 33 | )} 34 | 35 | 36 |
    37 | 38 |
    39 | ); 40 | }; 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /src/components/hooks/useScreenMedia.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | interface Query { 4 | [key: string]: boolean; 5 | } 6 | 7 | const useScreenMedia = () => { 8 | const [mediaQueries, setMediaQueries] = useState({ 9 | sm: false, 10 | md: false, 11 | lg: false, 12 | xl: false, 13 | }); 14 | 15 | useEffect(() => { 16 | const setNewMediaValues = (size?: string) => { 17 | setMediaQueries((prevState: Query) => { 18 | const prevStateCopy = { ...prevState }; 19 | 20 | for (const key in prevStateCopy) { 21 | prevStateCopy[key] = false; 22 | } 23 | if (size) return { ...prevStateCopy, [size]: true }; 24 | return { ...prevStateCopy }; 25 | }); 26 | }; 27 | 28 | const checkMediaSize = () => { 29 | if (window.innerWidth >= 1280) { 30 | setNewMediaValues("xl"); 31 | } else if (window.innerWidth >= 1024) { 32 | setNewMediaValues("lg"); 33 | } else if (window.innerWidth >= 768) { 34 | setNewMediaValues("md"); 35 | } else if (window.innerWidth >= 640) { 36 | setNewMediaValues("sm"); 37 | } else if (window.innerWidth > 0) { 38 | setNewMediaValues(); 39 | } 40 | }; 41 | 42 | checkMediaSize(); 43 | window.addEventListener("resize", checkMediaSize); 44 | return () => window.removeEventListener("resize", checkMediaSize); 45 | }, []); 46 | 47 | return mediaQueries; 48 | }; 49 | 50 | export default useScreenMedia; 51 | -------------------------------------------------------------------------------- /src/components/TasksSection/TaskItem/BtnEditTask.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useAppDispatch } from "../../../store/hooks"; 3 | import { tasksActions } from "../../../store/Tasks.store"; 4 | import ModalCreateTask from "../../Utilities/ModalTask"; 5 | import { ReactComponent as OptionsSvg } from "../../../assets/options.svg"; 6 | import { Task } from "../../../interfaces"; 7 | 8 | const BtnEditTask: React.FC<{ task: Task }> = ({ task }) => { 9 | const [modalEditTaskOpen, setModalEditTaskOpen] = useState(false); 10 | const dispatch = useAppDispatch(); 11 | 12 | const closeModalEditTask = () => { 13 | setModalEditTaskOpen(false); 14 | }; 15 | 16 | const openModalEditTask = () => { 17 | setModalEditTaskOpen(true); 18 | }; 19 | 20 | const editTaskHandler = (task: Task) => { 21 | dispatch(tasksActions.editTask(task)); 22 | }; 23 | 24 | return ( 25 | <> 26 | 33 | {modalEditTaskOpen && ( 34 | 40 | )} 41 | 42 | ); 43 | }; 44 | 45 | export default BtnEditTask; 46 | -------------------------------------------------------------------------------- /src/components/TasksSection/TasksSection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Navigate, Route, Routes } from "react-router-dom"; 3 | import Directory from "../Routes/Directory"; 4 | import DoneTasks from "../Routes/DoneTasks"; 5 | import Home from "../Routes/Home"; 6 | import ImportantTasks from "../Routes/ImportantTasks"; 7 | import SearchResults from "../Routes/SearchResults"; 8 | import TaskOnly from "../Routes/TaskOnly"; 9 | import TodaysTasks from "../Routes/TodaysTasks"; 10 | import HeaderTasks from "./HeaderTasks"; 11 | 12 | const TasksSection: React.FC = () => { 13 | return ( 14 |
    15 | 16 | 17 | } /> 18 | } /> 19 | } /> 20 | } 23 | /> 24 | } 27 | /> 28 | } /> 29 | } /> 30 | } /> 31 | } /> 32 | 33 |
    34 | ); 35 | }; 36 | 37 | export default TasksSection; 38 | -------------------------------------------------------------------------------- /src/components/TasksSection/TaskItem/BtnToggleCompleted.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useAppDispatch } from "../../../store/hooks"; 3 | import { tasksActions } from "../../../store/Tasks.store"; 4 | import { ReactComponent as SvgX } from "../../../assets/x.svg"; 5 | import { ReactComponent as Check } from "../../../assets/check.svg"; 6 | 7 | const BtnToggleCompleted: React.FC<{ 8 | taskCompleted: boolean; 9 | taskId: string; 10 | isListInView1: boolean; 11 | }> = ({ taskCompleted, taskId, isListInView1 }) => { 12 | const dispatch = useAppDispatch(); 13 | 14 | const toggleTaskCompleted = (id: string) => { 15 | dispatch(tasksActions.toggleTaskCompleted(id)); 16 | }; 17 | 18 | return ( 19 | 39 | ); 40 | }; 41 | 42 | export default React.memo(BtnToggleCompleted); 43 | -------------------------------------------------------------------------------- /src/components/AccountSection/DarkMode.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | const DarkMode: React.FC = () => { 4 | const [isCurrentDarkmode, setIsCurrentDarkmode] = useState(() => { 5 | const darkModeWasSet = localStorage.getItem("darkmode"); 6 | if (darkModeWasSet) return true; 7 | else return false; 8 | }); 9 | const toggleDarkMode = () => { 10 | setIsCurrentDarkmode((prevState) => !prevState); 11 | }; 12 | 13 | useEffect(() => { 14 | const html = document.querySelector("html")!; 15 | if (isCurrentDarkmode) { 16 | html.classList.add("dark"); 17 | localStorage.setItem("darkmode", "true"); 18 | document 19 | .querySelector('meta[name="theme-color"]') 20 | ?.setAttribute("content", "#0f172a"); 21 | } else { 22 | html.classList.remove("dark"); 23 | localStorage.removeItem("darkmode"); 24 | document 25 | .querySelector('meta[name="theme-color"]') 26 | ?.setAttribute("content", "#e2e8f0"); 27 | } 28 | }, [isCurrentDarkmode]); 29 | 30 | return ( 31 | 40 | ); 41 | }; 42 | 43 | export default React.memo(DarkMode); 44 | -------------------------------------------------------------------------------- /src/components/AccountSection/AccountData.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import avatar1 from "../../assets/avatar-1.jpg"; 3 | import { useAppDispatch, useAppSelector } from "../../store/hooks"; 4 | import { menusActions } from "../../store/Menu.store"; 5 | import LayoutMenus from "../Utilities/LayoutMenus"; 6 | import DarkMode from "./DarkMode"; 7 | import DeleteTasks from "./DeleteTasks"; 8 | import TasksDone from "./TasksDone"; 9 | 10 | const AccountData: React.FC = () => { 11 | const menuOpen = useAppSelector((state) => state.menu.menuAccountOpened); 12 | 13 | const dispatch = useAppDispatch(); 14 | 15 | const closeMenuHandler = () => { 16 | dispatch(menusActions.closeMenuAccount()); 17 | }; 18 | 19 | return ( 20 | 25 |
    26 | 27 | Hi, User! 28 | cat 29 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | Projected by Ibrahim Laklaa 40 | 41 |
    42 |
    43 | ); 44 | }; 45 | 46 | export default AccountData; 47 | -------------------------------------------------------------------------------- /src/components/Utilities/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { ReactComponent as SvgX } from "../../assets/x.svg"; 4 | 5 | const ModalContent: React.FC<{ 6 | children: React.ReactNode; 7 | onClose: () => void; 8 | title: string; 9 | }> = ({ children, onClose, title }) => { 10 | const closeModalHandler = (event: React.MouseEvent) => { 11 | if (event.target === event.currentTarget) { 12 | onClose(); 13 | } 14 | }; 15 | 16 | return ( 17 |
    21 |
    22 | 29 |

    {title}

    30 | {children} 31 |
    32 |
    33 | ); 34 | }; 35 | 36 | const modalElement = document.getElementById("modal")! as HTMLElement; 37 | 38 | const Modal: React.FC<{ 39 | children: React.ReactNode; 40 | onClose: () => void; 41 | title: string; 42 | }> = ({ children, onClose, title }) => { 43 | return ReactDOM.createPortal( 44 | , 45 | modalElement 46 | ); 47 | }; 48 | 49 | export default Modal; 50 | -------------------------------------------------------------------------------- /src/components/TasksSection/ButtonsSort.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ReactComponent as IconView1 } from "../../assets/view-1.svg"; 3 | import { ReactComponent as IconView2 } from "../../assets/view-2.svg"; 4 | 5 | const sortValues = [ 6 | { value: "order-added", title: "Order added" }, 7 | { value: "min-date", title: "Earlier first" }, 8 | { value: "max-date", title: "Later first" }, 9 | { value: "completed-first", title: "Completed first" }, 10 | { value: "uncompleted-first", title: "Uncompleted first" }, 11 | ]; 12 | 13 | const ButtonsSort: React.FC<{ 14 | isListInView1: boolean; 15 | sortedBy: string; 16 | setSortedBy: (option: string) => void; 17 | setIsListInView1: (status: boolean) => void; 18 | }> = ({ isListInView1, setIsListInView1, sortedBy, setSortedBy }) => { 19 | return ( 20 |
    21 | 24 | 27 | 45 |
    46 | ); 47 | }; 48 | 49 | export default ButtonsSort; 50 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 | 32 |
    33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/Menu/Directories/ContentDirectories.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useAppDispatch, useAppSelector } from "../../../store/hooks"; 3 | import { tasksActions } from "../../../store/Tasks.store"; 4 | import ModalDirectory from "../../Utilities/ModalDirectory"; 5 | import ItemDirectory from "./ItemDirectory"; 6 | 7 | const ContentDirectories: React.FC<{ classActive: string }> = ({ 8 | classActive, 9 | }) => { 10 | const directories = useAppSelector((store) => store.tasks.directories); 11 | const [modalDirIsShown, setModalDirIsShown] = useState(false); 12 | 13 | const dispatch = useAppDispatch(); 14 | 15 | const createNewDirectoryHandler = (inputValue: string) => { 16 | const newDirectoryName: string = inputValue.trim(); 17 | 18 | if (newDirectoryName.length === 0) return; 19 | 20 | const directoryDoesNotExist = directories.every( 21 | (dir: string) => dir !== newDirectoryName 22 | ); 23 | 24 | if (directoryDoesNotExist) { 25 | dispatch(tasksActions.createDirectory(newDirectoryName)); 26 | } 27 | }; 28 | 29 | const closeModalDirectoryHandler = () => { 30 | setModalDirIsShown(false); 31 | }; 32 | 33 | return ( 34 | <> 35 | {modalDirIsShown && ( 36 | 42 | )} 43 | 44 |
      45 | {directories.map((dir: string) => ( 46 | 47 | ))} 48 |
    49 | 55 | 56 | ); 57 | }; 58 | 59 | export default ContentDirectories; 60 | -------------------------------------------------------------------------------- /src/components/hooks/useSortTasks.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Task } from "../../interfaces"; 3 | 4 | const useSortTasks = (tasks: Task[]) => { 5 | const [sortedBy, setSortedBy] = useState(""); 6 | 7 | const [sortedTasks, setSortedTasks] = useState(tasks); 8 | 9 | useEffect(() => { 10 | const sortByDate = (order: "max-date" | "min-date"): Task[] => { 11 | const toMillisseconds = (date: string) => Date.parse(date); 12 | const tasksCopy = [...tasks]; 13 | const sorted = tasksCopy.sort((task1, task2) => { 14 | const date1 = toMillisseconds(task1.date); 15 | const date2 = toMillisseconds(task2.date); 16 | 17 | if (date1 < date2) { 18 | return -1; 19 | } 20 | 21 | if (date1 > date2) { 22 | return 1; 23 | } 24 | 25 | return 0; 26 | }); 27 | 28 | if (order === "min-date") { 29 | return sorted; 30 | } 31 | 32 | if (order === "max-date") { 33 | return sorted.reverse(); 34 | } 35 | 36 | return tasks; //se não existir tasks (para não retornar undefined) 37 | }; 38 | 39 | const sortByCompletedStatus = (completed: boolean): Task[] => { 40 | const tasksCopy = [...tasks]; 41 | const sorted = tasksCopy.sort((task1) => { 42 | if (task1.completed) { 43 | return -1; 44 | } 45 | return 0; 46 | }); 47 | if (completed) { 48 | return sorted; 49 | } 50 | if (!completed) { 51 | return sorted.reverse(); 52 | } 53 | return tasks; 54 | }; 55 | 56 | if (sortedBy === "min-date" || sortedBy === "max-date") { 57 | setSortedTasks(sortByDate(sortedBy)); 58 | } 59 | if (sortedBy === "" || sortedBy === "order-added") { 60 | setSortedTasks(tasks); 61 | } 62 | if (sortedBy === "completed-first") { 63 | setSortedTasks(sortByCompletedStatus(true)); 64 | } 65 | if (sortedBy === "uncompleted-first") { 66 | setSortedTasks(sortByCompletedStatus(false)); 67 | } 68 | }, [sortedBy, tasks]); 69 | return { sortedBy, setSortedBy, sortedTasks }; 70 | }; 71 | 72 | export default useSortTasks; 73 | -------------------------------------------------------------------------------- /src/components/Utilities/ModalDirectory.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useAppSelector } from "../../store/hooks"; 3 | import Modal from "./Modal"; 4 | 5 | const ModalDirectory: React.FC<{ 6 | onClose: () => void; 7 | dirName?: string; 8 | onConfirm: (newDirName: string) => void; 9 | btnText: string; 10 | title: string; 11 | }> = ({ onClose, dirName, onConfirm, btnText, title }) => { 12 | const directories = useAppSelector((store) => store.tasks.directories); 13 | 14 | const [errorDirectoryName, setErrorDirectoryName] = useState(false); 15 | const [newDirName, setNewDirName] = useState(dirName ? dirName : ""); 16 | 17 | const checkDirNameExists = (val: string) => { 18 | const directoryDoesNotExist = directories.every( 19 | (dir: string) => dir !== val 20 | ); 21 | 22 | if (directoryDoesNotExist || dirName === val) { 23 | setErrorDirectoryName(false); 24 | } else { 25 | setErrorDirectoryName(true); 26 | } 27 | }; 28 | 29 | const confirmDirNameHandler = (e: React.MouseEvent) => { 30 | e.preventDefault(); 31 | if (errorDirectoryName) return; 32 | onConfirm(newDirName); 33 | onClose(); 34 | }; 35 | 36 | return ( 37 | 38 |
    39 |
    40 | 43 | setNewDirName(target.value)} 49 | className={`inputStyles block w-full`} 50 | onInput={({ currentTarget }) => 51 | checkDirNameExists(currentTarget.value) 52 | } 53 | /> 54 | {errorDirectoryName && ( 55 |
    56 | Directory name already exists 57 |
    58 | )} 59 |
    60 | 63 |
    64 |
    65 | ); 66 | }; 67 | 68 | export default ModalDirectory; 69 | -------------------------------------------------------------------------------- /src/components/Utilities/LayoutRoutes.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Task } from "../../interfaces"; 3 | import { useAppDispatch } from "../../store/hooks"; 4 | import { modalActions } from "../../store/Modal.store"; 5 | import useSortTasks from "../hooks/useSortTasks"; 6 | import ButtonsSort from "../TasksSection/ButtonsSort"; 7 | import TaskItem from "../TasksSection/TaskItem/TaskItem"; 8 | 9 | type Props = { 10 | title: string; 11 | tasks: Task[] | []; 12 | }; 13 | 14 | const LayoutRoutes: React.FC = ({ title, tasks }) => { 15 | const [isListInView1, setIsListInView1] = useState(false); 16 | 17 | const dispatch = useAppDispatch(); 18 | 19 | const { sortedBy, setSortedBy, sortedTasks } = useSortTasks(tasks); 20 | 21 | const openModalHandler = () => { 22 | dispatch(modalActions.openModalCreateTask()); 23 | }; 24 | 25 | const tasksTitle = `${title} (${tasks.length} ${ 26 | tasks.length === 1 ? "task" : "tasks" 27 | })`; 28 | 29 | return ( 30 |
    31 |

    32 | {tasksTitle} 33 |

    34 | 40 |
      47 | {sortedTasks.map((task) => ( 48 | 49 | ))} 50 |
    • 51 | 63 |
    • 64 |
    65 |
    66 | ); 67 | }; 68 | 69 | export default React.memo(LayoutRoutes); 70 | -------------------------------------------------------------------------------- /src/components/TasksSection/HeaderTasks.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import BtnAddTask from "../Utilities/BtnAddTask"; 3 | import { ReactComponent as MenuIcon } from "../../assets/menu.svg"; 4 | import avatar1 from "../../assets/avatar-1.jpg"; 5 | import SearchField from "./SearchField"; 6 | import { useAppDispatch } from "../../store/hooks"; 7 | import { menusActions } from "../../store/Menu.store"; 8 | import Notification from "./Notification"; 9 | 10 | const HeaderTasks: React.FC = () => { 11 | const dispatch = useAppDispatch(); 12 | 13 | const date: Date = new Date(); 14 | const year: number = date.getFullYear(); 15 | const month: number = date.getMonth(); 16 | const day: number = date.getDate(); 17 | 18 | const monthName: string[] = [ 19 | "January", 20 | "February", 21 | "March", 22 | "April", 23 | "May", 24 | "June", 25 | "July", 26 | "August", 27 | "September", 28 | "October", 29 | "November", 30 | "December", 31 | ]; 32 | 33 | const todayDate = `${year}, ${monthName[month].slice(0, 3)} ${day 34 | .toString() 35 | .padStart(2, "0")}`; 36 | 37 | const dateTimeFormat = `${year}-${month.toString().padStart(2, "0")}-${day 38 | .toString() 39 | .padStart(2, "0")}}`; 40 | 41 | const openMenuHeaderHandler = () => { 42 | dispatch(menusActions.openMenuHeader()); 43 | }; 44 | const openMenuAccountHandler = () => { 45 | dispatch(menusActions.openMenuAccount()); 46 | }; 47 | 48 | return ( 49 |
    50 | 57 | 58 |
    59 | 60 | To-do list 61 | 62 | 63 |
    64 |
    65 | 66 | 67 | 68 | 75 |
    76 |
    77 | ); 78 | }; 79 | 80 | export default HeaderTasks; 81 | -------------------------------------------------------------------------------- /src/components/AccountSection/TasksDone.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { useAppSelector } from "../../store/hooks"; 4 | import useCompletedTasks from "../hooks/useCompletedTasks"; 5 | import useTodayTasks from "../hooks/useTodayTasks"; 6 | 7 | const TasksDone: React.FC = () => { 8 | const todaysTasks = useTodayTasks(); 9 | const tasks = useAppSelector((state) => state.tasks.tasks); 10 | const { tasks: todayTasksDone } = useCompletedTasks({ 11 | tasks: todaysTasks, 12 | done: true, 13 | }); 14 | const { tasks: allTasksDone } = useCompletedTasks({ 15 | tasks: tasks, 16 | done: true, 17 | }); 18 | 19 | const percentageTodayTasks = 20 | (todayTasksDone.length * 100) / todaysTasks.length; 21 | 22 | const percentageAllTasks = (allTasksDone.length * 100) / tasks.length; 23 | 24 | const todaysTasksToShow = todaysTasks.slice(0, 3); 25 | 26 | const showMore = todaysTasks.length > todaysTasksToShow.length; 27 | 28 | return ( 29 | <> 30 | {todaysTasks.length !== 0 && ( 31 |
    32 | 33 | Tasks today {todayTasksDone.length}/ 34 | {todaysTasks.length} 35 | 36 |
    37 |
    38 |
    39 |
    40 | )} 41 | {tasks.length !== 0 && ( 42 |
    43 | 44 | All tasks {allTasksDone.length}/{tasks.length} 45 | 46 |
    47 |
    48 |
    49 |
    50 | )} 51 | 52 | {todaysTasks.length === 0 && ( 53 | 54 | No tasks today 55 | 56 | )} 57 | 58 | {todaysTasks.length > 0 && ( 59 |
    60 | Today's tasks 61 |
      62 | {todaysTasksToShow.map((task) => ( 63 |
    • 64 | {task.title} 65 |
    • 66 | ))} 67 |
    68 | {showMore && ( 69 | 70 | Show more 71 | 72 | )} 73 |
    74 | )} 75 | 76 | ); 77 | }; 78 | 79 | export default React.memo(TasksDone); 80 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .inputStyles { 6 | @apply bg-slate-100 text-slate-600 dark:text-slate-200 rounded-lg p-3 outline-transparent border-2 border-transparent hover:border-violet-600 focus:border-violet-600 focus:outline-none transition dark:bg-slate-800; 7 | } 8 | 9 | .btn { 10 | @apply bg-violet-600 hover:bg-violet-700 py-3 px-6 text-slate-50 rounded-lg w-auto transition dark:bg-violet-800 dark:hover:bg-violet-900; 11 | } 12 | 13 | .btn:active { 14 | @apply scale-95; 15 | } 16 | 17 | @layer components { 18 | .tasksList > li:first-of-type article { 19 | @apply bg-violet-600 text-slate-100 dark:bg-violet-800; 20 | } 21 | .tasksList > li:first-of-type article .description { 22 | @apply text-violet-300; 23 | } 24 | .tasksList > li:first-of-type article > div { 25 | @apply border-violet-500 dark:border-violet-700; 26 | } 27 | .listView1 article { 28 | @apply h-40; 29 | } 30 | .listView2 article { 31 | @apply h-64; 32 | } 33 | .children-styles { 34 | @apply text-slate-400; 35 | } 36 | .children-styles button { 37 | @apply p-1; 38 | } 39 | .children-styles svg { 40 | @apply w-6 h-6; 41 | } 42 | .inputStyles::placeholder { 43 | @apply text-slate-400 dark:text-slate-400; 44 | } 45 | .stylesInputsField input, 46 | .stylesInputsField textarea, 47 | .stylesInputsField select { 48 | @apply inputStyles mt-1; 49 | } 50 | .stylesInputsField label { 51 | @apply mb-4 text-slate-600 dark:text-slate-500; 52 | } 53 | .barProgress { 54 | @apply bg-slate-200 w-full h-2 rounded-full overflow-hidden dark:bg-slate-700/[.3]; 55 | } 56 | .barProgress > div { 57 | @apply bg-violet-600 h-full; 58 | } 59 | .itemDirectory > .buttonsDir { 60 | @apply xl:opacity-0 transition opacity-100; 61 | } 62 | .itemDirectory:hover > .buttonsDir { 63 | @apply opacity-100; 64 | } 65 | } 66 | 67 | /* clears the 'X' from Internet Explorer */ 68 | input[type="search"]::-ms-clear { 69 | display: none; 70 | width: 0; 71 | height: 0; 72 | } 73 | input[type="search"]::-ms-reveal { 74 | display: none; 75 | width: 0; 76 | height: 0; 77 | } 78 | 79 | /* clears the 'X' from Chrome */ 80 | input[type="search"]::-webkit-search-decoration, 81 | input[type="search"]::-webkit-search-cancel-button, 82 | input[type="search"]::-webkit-search-results-button, 83 | input[type="search"]::-webkit-search-results-decoration { 84 | display: none; 85 | } 86 | 87 | ::-webkit-scrollbar { 88 | @apply w-2; 89 | } 90 | 91 | /* Track */ 92 | ::-webkit-scrollbar-track { 93 | @apply bg-slate-200 dark:bg-slate-800; 94 | } 95 | 96 | /* Handle */ 97 | ::-webkit-scrollbar-thumb { 98 | @apply bg-slate-300 dark:bg-slate-700 rounded-md border border-red-400; 99 | } 100 | 101 | /* Handle on hover */ 102 | ::-webkit-scrollbar-thumb:hover { 103 | @apply bg-slate-400 dark:bg-slate-600; 104 | } 105 | -------------------------------------------------------------------------------- /src/components/TasksSection/Notification.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import useVisibility from "../hooks/useVisibility"; 3 | import { ReactComponent as IconBell } from "../../assets/bell.svg"; 4 | import useTodayTasks from "../hooks/useTodayTasks"; 5 | import useCompletedTasks from "../hooks/useCompletedTasks"; 6 | import { Link } from "react-router-dom"; 7 | 8 | const classHasNotification = 9 | "after:content-[''] after:w-2 after:h-2 after:bg-rose-500 block after:rounded-full after:absolute after:bottom-3/4 after:left-3/4"; 10 | 11 | const Notification: React.FC = () => { 12 | const refBtnNotification = useRef(null); 13 | 14 | const { 15 | elementIsVisible: notificationIsVisible, 16 | showElement: showNotifications, 17 | } = useVisibility([refBtnNotification.current!]); 18 | 19 | const todaysTasks = useTodayTasks(); 20 | 21 | const { tasks: uncompletedTasks } = useCompletedTasks({ 22 | tasks: todaysTasks, 23 | done: false, 24 | }); 25 | 26 | const tasksToShow = uncompletedTasks.slice(0, 3); 27 | 28 | const moreTasksToShow = uncompletedTasks.length > tasksToShow.length; 29 | return ( 30 |
    31 | 39 | {notificationIsVisible && ( 40 |
    41 | {uncompletedTasks.length > 0 ? ( 42 |
    43 | 44 | You have {uncompletedTasks.length} uncompleted tasks today: 45 | 46 |
      47 | {tasksToShow.map((task) => ( 48 |
    • 49 | 53 | {task.title} 54 | 55 |
    • 56 | ))} 57 |
    58 | {moreTasksToShow && ( 59 | 63 | See today's tasks 64 | 65 | )} 66 |
    67 | ) : ( 68 |

    Nothing to show here.

    69 | )} 70 |
    71 | )} 72 |
    73 | ); 74 | }; 75 | 76 | export default React.memo(Notification); 77 | -------------------------------------------------------------------------------- /src/components/Menu/Directories/ItemDirectory.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { NavLink, useLocation } from "react-router-dom"; 3 | import { useAppDispatch } from "../../../store/hooks"; 4 | import { tasksActions } from "../../../store/Tasks.store"; 5 | import { ReactComponent as Trash } from "../../../assets/trash.svg"; 6 | import { ReactComponent as Edit } from "../../../assets/edit.svg"; 7 | import ModalConfirm from "../../Utilities/ModalConfirm"; 8 | import ModalDirectory from "../../Utilities/ModalDirectory"; 9 | 10 | const ItemDirectory: React.FC<{ dir: string; classActive: string }> = ({ 11 | dir, 12 | classActive, 13 | }) => { 14 | const route = useLocation(); 15 | const currentPath = route.pathname; 16 | 17 | const dispatch = useAppDispatch(); 18 | 19 | const [modalIsShown, setModalIsShown] = useState(false); 20 | const [modalDirIsShown, setModalDirIsShown] = useState(false); 21 | 22 | const closeModalDirectoryHandler = () => { 23 | setModalDirIsShown(false); 24 | }; 25 | 26 | const deleteDirectoryHandler = () => { 27 | dispatch(tasksActions.deleteDirectory(dir)); 28 | }; 29 | 30 | const confirmEditDirNameHandler = (dirName: string) => { 31 | dispatch( 32 | tasksActions.editDirectoryName({ 33 | previousDirName: dir, 34 | newDirName: dirName, 35 | }) 36 | ); 37 | }; 38 | 39 | return ( 40 | <> 41 | {modalDirIsShown && ( 42 | 49 | )} 50 | {modalIsShown && ( 51 | setModalIsShown(false)} 53 | onConfirm={deleteDirectoryHandler} 54 | text="This directory and all its tasks will be deleted." 55 | /> 56 | )} 57 |
  • 62 | 67 | {dir} 68 | 69 | 70 | {dir !== "Main" && ( 71 |
    72 | 78 | 84 |
    85 | )} 86 |
  • 87 | 88 | ); 89 | }; 90 | 91 | export default ItemDirectory; 92 | -------------------------------------------------------------------------------- /src/components/TasksSection/SearchField.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { createSearchParams, Link, useNavigate } from "react-router-dom"; 3 | import { ReactComponent as Search } from "../../assets/search.svg"; 4 | import { Task } from "../../interfaces"; 5 | import useDate from "../hooks/useDate"; 6 | import useSearchQuery from "../hooks/useSearchQuery"; 7 | import useVisibility from "../hooks/useVisibility"; 8 | 9 | const ItemSearch: React.FC<{ task: Task }> = ({ task }) => { 10 | const dateFormated = useDate(task.date); 11 | return ( 12 |
  • 13 | 17 | {task.title} 18 | {dateFormated} 19 | 20 |
  • 21 | ); 22 | }; 23 | 24 | const SearchField: React.FC = () => { 25 | const navigate = useNavigate(); 26 | 27 | const searchResultsRef = useRef(null); 28 | const [searchInputValue, setSearchInputValue] = useState(""); 29 | 30 | const matchedTasks = useSearchQuery(searchInputValue); 31 | 32 | const tasks = matchedTasks.slice(0, 4); 33 | 34 | const { 35 | elementIsVisible: listResultsVisible, 36 | showElement: showListResults, 37 | closeElement: closeListResults, 38 | } = useVisibility([searchResultsRef.current!], () => setSearchInputValue("")); 39 | 40 | const navigateToSearchResults = () => { 41 | navigate({ 42 | pathname: "results", 43 | search: createSearchParams({ 44 | q: searchInputValue, 45 | }).toString(), 46 | }); 47 | }; 48 | 49 | useEffect(() => { 50 | if (searchInputValue.trim().length > 0) { 51 | showListResults(); 52 | } else { 53 | closeListResults(); 54 | } 55 | }, [closeListResults, searchInputValue, showListResults]); 56 | 57 | return ( 58 |
    59 |
    60 | 61 | { 67 | setSearchInputValue(currentTarget.value); 68 | }} 69 | className="inputStyles w-full" 70 | /> 71 | 72 | {listResultsVisible && ( 73 |
    74 | {tasks.length ? ( 75 | <> 76 |
      77 | {tasks.map((task) => ( 78 | 79 | ))} 80 |
    81 | 87 | 88 | ) : ( 89 | No tasks found 90 | )} 91 |
    92 | )} 93 | 94 |
    95 | ); 96 | }; 97 | 98 | export default SearchField; 99 | -------------------------------------------------------------------------------- /src/components/Utilities/ModalTask.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from "react"; 2 | import { Task } from "../../interfaces"; 3 | import { useAppSelector } from "../../store/hooks"; 4 | import Modal from "./Modal"; 5 | 6 | const InputCheckbox: React.FC<{ 7 | label: string; 8 | isChecked: boolean; 9 | setChecked: (value: React.SetStateAction) => void; 10 | }> = ({ isChecked, setChecked, label }) => { 11 | return ( 12 | 26 | ); 27 | }; 28 | 29 | const ModalCreateTask: React.FC<{ 30 | onClose: () => void; 31 | task?: Task; 32 | nameForm: string; 33 | onConfirm: (task: Task) => void; 34 | }> = ({ onClose, task, nameForm, onConfirm }) => { 35 | const directories = useAppSelector((state) => state.tasks.directories); 36 | 37 | const today: Date = new Date(); 38 | let day: number = today.getDate(); 39 | let month: number = today.getMonth() + 1; 40 | const year: number = today.getFullYear(); 41 | if (day < 10) { 42 | day = +("0" + day); 43 | } 44 | if (month < 10) { 45 | month = +("0" + month); 46 | } 47 | 48 | const todayDate: string = year + "-" + month + "-" + day; 49 | const maxDate: string = year + 1 + "-" + month + "-" + day; 50 | 51 | const [description, setDescription] = useState(() => { 52 | if (task) { 53 | return task.description; 54 | } 55 | return ""; 56 | }); 57 | const [title, setTitle] = useState(() => { 58 | if (task) { 59 | return task.title; 60 | } 61 | return ""; 62 | }); 63 | const [date, setDate] = useState(() => { 64 | if (task) { 65 | return task.date; 66 | } 67 | return todayDate; 68 | }); 69 | const isTitleValid = useRef(false); 70 | const isDateValid = useRef(false); 71 | 72 | const [isImportant, setIsImportant] = useState(() => { 73 | if (task) { 74 | return task.important; 75 | } 76 | return false; 77 | }); 78 | 79 | const [isCompleted, setIsCompleted] = useState(() => { 80 | if (task) { 81 | return task.completed; 82 | } 83 | return false; 84 | }); 85 | 86 | const [selectedDirectory, setSelectedDirectory] = useState(() => { 87 | if (task) { 88 | return task.dir; 89 | } 90 | return directories[0]; 91 | }); 92 | 93 | const addNewTaskHandler = (event: React.FormEvent): void => { 94 | event.preventDefault(); 95 | 96 | isTitleValid.current = title.trim().length > 0; 97 | isDateValid.current = date.trim().length > 0; 98 | 99 | if (isTitleValid.current && isDateValid.current) { 100 | const newTask: Task = { 101 | title: title, 102 | dir: selectedDirectory, 103 | description: description, 104 | date: date, 105 | completed: isCompleted, 106 | important: isImportant, 107 | id: task?.id ? task.id : Date.now().toString(), 108 | }; 109 | onConfirm(newTask); 110 | onClose(); 111 | } 112 | }; 113 | return ( 114 | 115 |
    119 | 130 | 142 | 151 | 169 | 174 | 179 | 182 | 183 |
    184 | ); 185 | }; 186 | 187 | export default ModalCreateTask; 188 | -------------------------------------------------------------------------------- /src/store/Tasks.store.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | createSlice, 4 | Dispatch, 5 | MiddlewareAPI, 6 | PayloadAction, 7 | } from "@reduxjs/toolkit"; 8 | import { Task } from "../interfaces"; 9 | 10 | const defaultTasks: Task[] = [ 11 | { 12 | title: "Task 1", 13 | important: false, 14 | description: "This is the description for this task", 15 | date: "2023-04-12", 16 | dir: "Main", 17 | completed: true, 18 | id: "t1", 19 | }, 20 | { 21 | title: "Task 2", 22 | important: true, 23 | description: "This is the description for this task", 24 | date: "2023-05-15", 25 | dir: "Main", 26 | completed: true, 27 | id: "t2", 28 | }, 29 | { 30 | title: "Task 3", 31 | important: false, 32 | description: "This is the description for this task", 33 | date: "2023-08-21", 34 | dir: "Main", 35 | completed: false, 36 | id: "t3", 37 | }, 38 | ]; 39 | 40 | const getSavedDirectories = (): string[] => { 41 | let dirList: string[] = []; 42 | if (localStorage.getItem("directories")) { 43 | dirList = JSON.parse(localStorage.getItem("directories")!); 44 | const mainDirExists = dirList.some((dir: string) => dir === "Main"); 45 | if (!mainDirExists) { 46 | dirList.push("Main"); 47 | } 48 | } else { 49 | dirList.push("Main"); 50 | } 51 | 52 | if (localStorage.getItem("tasks")) { 53 | const savedTasksList = JSON.parse(localStorage.getItem("tasks")!); 54 | let dirNotSaved: string[] = []; 55 | savedTasksList.forEach((task: Task) => { 56 | if (!dirList.includes(task.dir)) { 57 | if (!dirNotSaved.includes(task.dir)) { 58 | dirNotSaved.push(task.dir); 59 | } 60 | } 61 | }); 62 | dirList = [...dirList, ...dirNotSaved]; 63 | } 64 | return dirList; 65 | }; 66 | 67 | const initialState: { 68 | tasks: Task[]; 69 | directories: string[]; 70 | } = { 71 | tasks: localStorage.getItem("tasks") 72 | ? JSON.parse(localStorage.getItem("tasks")!) 73 | : defaultTasks, 74 | directories: getSavedDirectories(), 75 | }; 76 | 77 | const tasksSlice = createSlice({ 78 | name: "tasks", 79 | initialState: initialState, 80 | reducers: { 81 | addNewTask(state, action: PayloadAction) { 82 | state.tasks = [action.payload, ...state.tasks]; 83 | }, 84 | removeTask(state, action) { 85 | const newTasksList = state.tasks.filter( 86 | (task) => task.id !== action.payload 87 | ); 88 | state.tasks = newTasksList; 89 | }, 90 | markAsImportant(state, action: PayloadAction) { 91 | const newTaskFavorited = state.tasks.find( 92 | (task) => task.id === action.payload 93 | ); 94 | newTaskFavorited!.important = !newTaskFavorited!.important; 95 | }, 96 | editTask(state, action: PayloadAction) { 97 | const taskId = action.payload.id; 98 | 99 | const newTaskEdited: Task = state.tasks.find( 100 | (task: Task) => task.id === taskId 101 | )!; 102 | const indexTask = state.tasks.indexOf(newTaskEdited); 103 | state.tasks[indexTask] = action.payload; 104 | }, 105 | toggleTaskCompleted(state, action: PayloadAction) { 106 | const taskId = action.payload; 107 | 108 | const currTask = state.tasks.find((task) => task.id === taskId)!; 109 | 110 | currTask.completed = !currTask.completed; 111 | }, 112 | deleteAllData(state) { 113 | state.tasks = []; 114 | state.directories = ["Main"]; 115 | }, 116 | createDirectory(state, action: PayloadAction) { 117 | const newDirectory: string = action.payload; 118 | const directoryAlreadyExists = state.directories.includes(newDirectory); 119 | if (directoryAlreadyExists) return; 120 | state.directories = [newDirectory, ...state.directories]; 121 | }, 122 | deleteDirectory(state, action: PayloadAction) { 123 | const dirName = action.payload; 124 | 125 | state.directories = state.directories.filter((dir) => dir !== dirName); 126 | state.tasks = state.tasks.filter((task) => task.dir !== dirName); 127 | }, 128 | editDirectoryName( 129 | state, 130 | action: PayloadAction<{ newDirName: string; previousDirName: string }> 131 | ) { 132 | const newDirName: string = action.payload.newDirName; 133 | const previousDirName: string = action.payload.previousDirName; 134 | const directoryAlreadyExists = state.directories.includes(newDirName); 135 | if (directoryAlreadyExists) return; 136 | 137 | const dirIndex = state.directories.indexOf(previousDirName); 138 | 139 | state.directories[dirIndex] = newDirName; 140 | state.tasks.forEach((task) => { 141 | if (task.dir === previousDirName) { 142 | task.dir = newDirName; 143 | } 144 | }); 145 | }, 146 | }, 147 | }); 148 | 149 | export const tasksActions = tasksSlice.actions; 150 | export default tasksSlice.reducer; 151 | 152 | export const tasksMiddleware = 153 | (store: MiddlewareAPI) => (next: Dispatch) => (action: Action) => { 154 | const nextAction = next(action); 155 | const actionChangeOnlyDirectories = 156 | tasksActions.createDirectory.match(action); 157 | 158 | const isADirectoryAction: boolean = action.type 159 | .toLowerCase() 160 | .includes("directory"); 161 | 162 | if (action.type.startsWith("tasks/") && !actionChangeOnlyDirectories) { 163 | const tasksList = store.getState().tasks.tasks; 164 | localStorage.setItem("tasks", JSON.stringify(tasksList)); 165 | } 166 | if (action.type.startsWith("tasks/") && isADirectoryAction) { 167 | const dirList = store.getState().tasks.directories; 168 | localStorage.setItem("directories", JSON.stringify(dirList)); 169 | } 170 | 171 | if (tasksActions.deleteAllData.match(action)) { 172 | localStorage.removeItem("tasks"); 173 | localStorage.removeItem("directories"); 174 | localStorage.removeItem("darkmode"); 175 | } 176 | 177 | if (tasksActions.removeTask.match(action)) { 178 | console.log(JSON.parse(localStorage.getItem("tasks")!)); 179 | if (localStorage.getItem("tasks")) { 180 | const localStorageTasks = JSON.parse(localStorage.getItem("tasks")!); 181 | if (localStorageTasks.length === 0) { 182 | localStorage.removeItem("tasks"); 183 | } 184 | } 185 | } 186 | return nextAction; 187 | }; 188 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | --------------------------------------------------------------------------------