diff --git a/.eslintignore b/.eslintignore index d8b83df9..483a9c42 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1 @@ -package-lock.json +package-lock.json \ No newline at end of file diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 00000000..31354ec1 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/README.md b/README.md index b8108178..16b08df1 100644 --- a/README.md +++ b/README.md @@ -1,380 +1,52 @@ -`#react.js` `#master-in-software-engineering` - +# React To Do List +Basic to do list with friendly UI, using react, local storage +and bootstrap. -[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) +## Appendix - +Any additional information goes here -# Assembler School: React.js Todo List - -In this project you will learn how to create a React.js todo list. - -## Table of Contents - -- [Getting Started](#getting-started) -- [The Project](#the-project) -- [Project requirements](#project-requirements) -- [Project delivery](#project-delivery) +- [Documentation](#documentation) +- [Lessons Learned](#lessons-learned) - [Resources](#resources) +- [Links](#links) +## Documentation -## Getting Started - -These instructions will get you a copy of the project up and running on your -local machine for development and testing purposes. - -See deployment for notes on how to deploy the project on a live system. - -### The repository - -First, you will need to `clone` or `fork` the repository into your Github -account: - -Fork on GitHub - -``` -$ git clone https://github.com/assembler-school/reactjs-todo-list.git -``` - -## Contents and Branches Naming Strategy - -The repository is made up of several branches that include the contents of each -section. - -The branches follow a naming strategy like the following: - -- `main`: includes the main contents and the instructions -- `assembler-solution`: includes the solution - -### Fetching All the Branches - -In order to fetch all the remote branches in the repository, you can use the -following command: - -```sh -$ git fetch --all -``` - -### List Both Remote Tracking Branches and Local Branches - -```sh -$ git branch --all -``` - -Then, you can create a local branch based on a remote branch with the following -command: - -```sh -$ git checkout -b -``` - -### Installing - -First, you will need to install the dependencies with: `npm install`. - -Run the following command in your terminal after cloning the main repo: - -```sh -$ npm install -``` - -### Running the Tests - -The tests that validate your solution can be executed by runing the following -commands: - -#### 1. Run the development server - -``` -$ npm run start -``` - -#### 2. Run the Cypress tests with the graphical test runner - -``` -$ npm run cy:open -``` - -#### 3. Or you can run the Cypress tests from the terminal - -``` -$ npm run cy:run -``` - -Both need the Create React App development server to be running. - -### Git `precommit` and `prepush` Hooks - -In the `assembler-solution` branch you can see an implementation of these tools -if you'd like to use them. - -## Deployment - -In this pill we won't deploy the app. - -## Technologies used - -- `React.js` -- `@testing-library/react` -- `eslint` -- `prettier` -- `lint-staged` -- `husky` - -## The Project - -In this project you will build a todo app similar to the following screenshot. - - - -## Project requirements - -This is an overview of the main requirements of this project. The exact ones are -found in the doc that the academic team will provide you. - -- You must follow all the instructions of the project step-by-step -- You should always try to solve them by yourself before asking for help -- You should always help your team members and fellow students of the master so - that you can all learn together and become better software developers and team - members -- You must finish all the steps that are marked as `Required` -- **You must use semantic HTML5 elements for all the markup of the application** -- Once you are done, you can move on to the optional ones that are marked as - `Extra 💯` - -### 1. Styles and Layout - -For this step you have to think of a layout for the app. - -1. You **must** use `SCSS` for all the styles of the app and the - [classnames](https://github.com/JedWatson/classnames#readme) npm package to - handle any conditionally set classes -2. The overall layout must be a pixel perfect copy of the design we provide -3. The layout must be responsive so that it works in all device sizes - -### 2. Show All the Todos - -In this step you must implement the logic to render all the todos of the app. -This means that all the todos are rendered without taking into account if they -are completed or not. +-[Original repo]( https://github.com/assembler-school/reactjs-todo-list ) -1. **The todos must be created in the `App` component and passed as props to the - page components** -2. **All the methods that modify the `` state must also be passed as - props** -3. If there are no todos created you must render a message telling the user that - they can create their first todo to get started -4. You can also render an illustration that indicates users that they can create - a todo to get started - 1. Feel free to create your own or use one from the internet, this is a great - resource: [undraw.co](https://undraw.co/illustrations) - 2. For this step, you will need to add a `data-testid="no-todos"` property on - the html component that is rendered as the illustration or the - illustration wrapper. + +## Lessons Learned -### 3. Creating Todos +- Use React + -to use props and states + -to organice and structure components 😭 -#### Step 1 +- To work in peer coding -Users must be able to create a new todo using the form in the app header +- Use SASS with React -1. To test the todo creation requirement you will need to add a - `data-testid="create-todo-input"` property on the input element -2. If the user presses enter without entering a value in the todo form, an - error message should be rendered. The message needs to have a - `data-testid="create-todo-error-message"` property with the following error - message: `"Please enter at least one character"` -3. Users must be able to press the `enter` key on their keyboard to create the - todo (if you implement it using semantic html5 this comes for free) - -#### Step 2 - -Once the todo is created it must be rendered in the list bellow the form. - -1. By default the list should be empty. -2. The list must be implemented using `ul` and `li` elements. -3. To test the todo creation requirement you will need to: - 1. add a `data-testid="todos-list"` property on the `ul` list element. - 2. add a `data-testid="todo-item"` property on the `li` list element. - 3. the `li` list element should have a text content of the todo that was - created and the `checkbox` value set to `checked` or not depending on if - the todo was marked as completed. - -#### Step 3 - -Render the total number of todos in the app footer. - -The footer should have a property of: `data-testid="app-footer"` and be -implemented using an html5 `footer` element. - -1. To test this requirement you will need to render the total number of todos - that the app has, that is both completed and incomplete todos. -2. The text rendered should be: - 1. `0 todos left`: if there are no todos - 2. `1 todos left`: if there is only 1 todo - 3. `12 todos left`: if there are 12 todos in the app - 4. etc - -### 4. Editing Todos - -Users must be able to edit the todos once they are created. - -1. Clicking the todo name should open a form that allows users to edit the todo - name. - 1. This can be implemented either in line or by filling out the new todo form - with the details of the todo that was clicked - 2. The todo item should have a property of `data-testid="todo-item-input"` - 3. We recommend that you implement a solution that allows users to edit the - todo in line. This means that clicking the todo name replaces the todo - with a form that has a value of the todo's name - 4. Then, by clicking on the done button or by pressing enter, the todo is - edited and saved -2. Users should be able to delete todos by clicking the `X` button that is - rendered when users hover over the todo name - 1. the `X` button should have a property of - `data-testid="todo-item-delete-button"` -3. Users must be able to mark a todo as completed when they press the `Done` - button that is rendered when the user hovers over the todo name - 1. the `X` button should have a property of - `data-testid="todo-item-checkbox"` - -### 5. Filtering Todos - -In this step you will create a page for each todo type. - -In order to allow users to navigate to a page you will need to complete the -footer of the app that you can see in the screenshot above. - -You will have to render the following in the footer: - -1. The total count of all the `active` todos -2. A link to the home page that renders `all` the todos, **both active and - completed** -3. A link to the active todos page that renders the `active` todos -4. A link to the completed todos page that renders the `completed` todos - -#### 5.1 All Todos - -- Route: `/` -- Page Component: `Home` - -In this page you will render all the todos, both completed or not. - -#### 5.2 Completed Todos - -- Route: `/completed` -- Page Component: `Completed` - -In this page you will render all the **completed** todos. - -You will need to think of a way to store or filter the todos that are completed. - -A possible solution is to use `[].filter` or to store the todos in a different -`this.state` property. - -Feel free to think of a solution for this requirement. - -#### 5.3 Active Todos - -- Route: `/active` -- Page Component: `Active` - -In this page you will render all the **active** todos, that is, all the todos -that are not completed. - -You will need to think of a way to store or filter the todos that are active. - -A possible solution is to use `[].filter` or to store the todos in a different -`this.state` property. - -Feel free to think of a solution for this requirement. - -### 💯 Extras - -#### 1. Store the todos in `localStorage` - -All the todos are stored in `localStorage` so that users can refresh the app and -their previous todos are not lost. - -You must store the todos in a single local storage entry named: -`"reactjs-todo-list"`. - -The todos should be stored using the following shape (you can add other -properties but these are required): - -- `id`: the id of the todo -- `text`: the text content of the todo -- `done`: boolean that indicates wether the todo is completed or not -- `isEditing`: boolean that indicates wether the todo is currently being edited - -```js -[ - { - id: "9c34e805-7bfc-401a-b386-468c25315e46", - text: "todo 01", - done: false, - isEditing: false, - }, - { - id: "d733a37e-cc4e-4cde-916f-935b3e915bb3", - text: "todo 02", - done: false, - isEditing: false, - }, -]; -``` - -#### 2. Clear all the Completed Todos - -Users must be able to clear all the todos that are completed. You can implement -a button in the app footer that allows users to clear the completed todos. - -The clear completed todos button should have a property of -`data-testid="clear-completed-todos"`. - -#### 3: Light & Dark Mode Switch - -You can implement a light & dark mode switch that can be toggled using the moon -icon in the app header. - -The light & dark mode switch button should have a property of -`data-testid="toggle-theme"`. - -## Project delivery - -To deliver this project you must follow the steps indicated in the document: - -- [Submitting a solution](https://www.notion.so/Submitting-a-solution-524dab1a71dd4b96903f26385e24cdb6) +- To design friendly UI with bootstrap + ## Resources -- [react-testing-library](https://testing-library.com/docs/react-testing-library/intro/) -- [reactjs.org](https://reactjs.org/) - -## License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file -for details - -## Contributors ✨ +- [WEBPACK](https://webpack.js.org/concepts/) +- [SASS](https://sass-lang.com/guide) +- [REACT](https://es.reactjs.org/) +- [BOOTSTRAP](https://getbootstrap.com/docs/4.6) -Thanks goes to these wonderful people -([emoji key](https://allcontributors.org/docs/en/emoji-key)): +## Authors - - - - - - - -

Dani Lucaci

💻 📖 💡 🔧
+- [@CHerreroTinoco](https://github.com/Cherrerotinoco) +- [@ParisArcos](https://github.com/ParisArcos) - - - + +## 🔗 Links +Paris Arcos[![linkedin](https://img.shields.io/badge/linkedin-0A66C2?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/paris-arcos-martin-268708217/) +Christian Herrero[![linkedin](https://img.shields.io/badge/linkedin-0A66C2?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/christian-herrero-tinoco/) -This project follows the -[all-contributors](https://github.com/all-contributors/all-contributors) -specification. Contributions of any kind welcome! +## Screenshots +![App Screenshot](./src/img/ToDoPlanning.png) + diff --git a/package.json b/package.json index 18a1117b..223770de 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "clsx": "^1.1.1", "cypress": "^7.5.0", "formik": "^2.2.6", + "lodash": "^4.17.21", "prop-types": "^15.7.2", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/src/App.js b/src/App.js index de524524..8b451b25 100644 --- a/src/App.js +++ b/src/App.js @@ -1,15 +1,95 @@ +/* eslint-disable prettier/prettier */ import React from "react"; +import { BrowserRouter } from "react-router-dom"; -function App() { - return ( -
-
-
-

Hola mundo

-
-
-
- ); -} +import AddToDoTask from "./components/AddToDoTask"; +import Footer from "./components/Footer/Footer"; +import Header from "./components/Header"; +import MainList from "./components/MainList"; +import FilterToDo from "./components/FilterToDo/FilterToDo"; +import { deleteItem, saveItem } from "./utils/localStorage"; +import Background from "./components/Background/Background"; +import Title from "./components/Title/Title"; + +export default class App extends React.Component { + constructor(props) { + super(props); + + this.state = { + tasks: [], + }; + } + + componentDidMount() { + // Then re-render MainList (props: task) & Footer (props: lenght) + this.refreshState(); + } + + handlerNewToDo = (task) => { + // Save Todo en localStorage + saveItem(task); + // Refresh state with new localStorage + this.refreshState(); + }; + + handlerDeleteTask = (id) => { + // Delete item from locaStorage + deleteItem(id); -export default App; + // Refresh state + this.refreshState(); + }; + + handlerClearCompleted = () => { + Object.values(localStorage).forEach((elm) => { + const task = JSON.parse(elm); + task.done ? deleteItem(task.id) : null; + }); + this.refreshState(); + }; + + refreshState = () => { + // Get items from localStorage + const localStorageTasks = Object.values(localStorage).map((elm) => + JSON.parse(elm), + ); + const sortedTasks = localStorageTasks.sort((a, b) => b.id - a.id); + // Refresh state with new localStorage + sortedTasks.length > 0 + ? this.setState({ tasks: [...sortedTasks] }) + : this.setState({ tasks: [] }); + }; + + render() { + const { tasks } = this.state; + + return ( + <> + +
+ + <section className="row justify-content-center"> + <div className="col col-sm-12 col-md-6 p-5 rounded bg-white shadow-lg"> + <Header> + <AddToDoTask handlerNewToDo={this.handlerNewToDo} /> + </Header> + <BrowserRouter> + <MainList + tasks={tasks} + handlerDeleteTask={this.handlerDeleteTask} + refreshState={this.refreshState} + /> + <Footer> + <FilterToDo + counter={tasks.length} + handlerClearCompleted={this.handlerClearCompleted} + /> + </Footer> + </BrowserRouter> + </div> + </section> + </main> + </> + ); + } +} diff --git a/src/components/AddToDoTask/AddToDoTask.js b/src/components/AddToDoTask/AddToDoTask.js new file mode 100644 index 00000000..78fec223 --- /dev/null +++ b/src/components/AddToDoTask/AddToDoTask.js @@ -0,0 +1,78 @@ +/* eslint-disable prettier/prettier */ +import React from "react"; +import "./AddToDoTask.scss"; +import { generateNewKey } from "../../utils/localStorage"; + +// Improve the render of the component + +export default class AddToDoTask extends React.Component { + constructor(props) { + super(props); + + this.state = { + title: "", + error: false, + }; + } + + formSubmit = (event) => { + event.preventDefault(); + + const { error, title } = this.state; + const { handlerNewToDo } = this.props; + + if (title.length === 0) this.setState({error: true}) + + if (!error && title !== "") { + const key = generateNewKey(); + const taskObj = { + id: key, + inputValue: title, + done: false, + isEditing: false, + }; + + // Send new task to App + handlerNewToDo(taskObj); + + // Reset input state + this.updateState({ title: "", error: false }); + } + }; + + handlerInput = (event) => { + const text = event.target.value; + const error = text.length === 0; + + this.updateState({ text, error }); + }; + + updateState = ({ text = "", error }) => { + this.setState({ title: text, error: error }); + }; + + render() { + const { title, error } = this.state; + return ( + <> + <form onSubmit={this.formSubmit} className="mb-2"> + <input + className="form-control" + placeholder="Do the homework..." + data-testid="create-todo-input" + type="text" + value={title} + onChange={this.handlerInput} + /> + </form> + <div + data-testid="create-todo-error-message" + className={error ? `show alert alert-danger` : `hide`} + role="alert" + > + Please enter at least one character + </div> + </> + ); + } +} diff --git a/src/components/AddToDoTask/AddToDoTask.scss b/src/components/AddToDoTask/AddToDoTask.scss new file mode 100644 index 00000000..f0bc119b --- /dev/null +++ b/src/components/AddToDoTask/AddToDoTask.scss @@ -0,0 +1,7 @@ +.show { + display: block; + color: red; +} +.hide { + display: none; +} \ No newline at end of file diff --git a/src/components/AddToDoTask/index.js b/src/components/AddToDoTask/index.js new file mode 100644 index 00000000..b020c385 --- /dev/null +++ b/src/components/AddToDoTask/index.js @@ -0,0 +1,3 @@ +import AddToDoTask from "./AddToDoTask"; + +export default AddToDoTask; diff --git a/src/components/Background/Background.js b/src/components/Background/Background.js new file mode 100644 index 00000000..c99810fd --- /dev/null +++ b/src/components/Background/Background.js @@ -0,0 +1,15 @@ +/* eslint-disable prettier/prettier */ +import React from "react"; + +import "./Background.scss"; + +export default class Background extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + return <div className="background_img" />; + } +} diff --git a/src/components/Background/Background.scss b/src/components/Background/Background.scss new file mode 100644 index 00000000..b730a5e6 --- /dev/null +++ b/src/components/Background/Background.scss @@ -0,0 +1,8 @@ +.background_img { + background-image: url('../../img/runny-run-homegiffy.gif'); + background-size: cover; + background-position: 10% 30%; + height: 45vh; + width: auto; + +} \ No newline at end of file diff --git a/src/components/Background/Index.js b/src/components/Background/Index.js new file mode 100644 index 00000000..6c762749 --- /dev/null +++ b/src/components/Background/Index.js @@ -0,0 +1,3 @@ +import Background from "./Background"; + +export default Background; diff --git a/src/components/FilterToDo/FilterToDo.js b/src/components/FilterToDo/FilterToDo.js new file mode 100644 index 00000000..c30a45a4 --- /dev/null +++ b/src/components/FilterToDo/FilterToDo.js @@ -0,0 +1,31 @@ +/* eslint-disable prettier/prettier */ +import React from 'react' +import { Link } from 'react-router-dom' +import './FilterToDo.scss' + +export default class FilterToDo extends React.Component { + + constructor(props) { + super(props) + this.state = {} + } + + + render() { + const {counter, handlerClearCompleted} = this.props + + return ( + <> + <nav className="navbar rounded flex-wrap"> + <span className="flex-fill small text-center">{counter > 1 ? `${counter} todos left` : `${counter} todo left` }</span> + <div className="navbar-nav flex-row flex-wrap justify-content-center"> + <Link to="/" className="nav-link">All</Link> + <Link to="/active" className="nav-link">Active</Link> + <Link to="/completed" className="nav-link">Completed</Link> + <button type="button" data-testid="clear-completed-todos" className="btn btn-primary" onClick={handlerClearCompleted}>Clear completed <span role="img" aria-label="checkbox">✔️</span></button> + </div> + </nav> + </> + ) + } +} \ No newline at end of file diff --git a/src/components/FilterToDo/FilterToDo.scss b/src/components/FilterToDo/FilterToDo.scss new file mode 100644 index 00000000..71223d6d --- /dev/null +++ b/src/components/FilterToDo/FilterToDo.scss @@ -0,0 +1,18 @@ +.navbar-nav .nav-link { + padding: 10px; + color: var(--gray-dark); + text-decoration: underline; + + &:hover { + color: var(--secondary); + } +} +.btn.btn-primary { + border-color: var(--purple); + background-color: var(--purple); + + &:active { + background-color: #815fc0 !important; + border-color: #815fc0 !important; + } +} \ No newline at end of file diff --git a/src/components/FilterToDo/index.js b/src/components/FilterToDo/index.js new file mode 100644 index 00000000..090bb22d --- /dev/null +++ b/src/components/FilterToDo/index.js @@ -0,0 +1 @@ +export { default } from "./FilterToDo"; diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js new file mode 100644 index 00000000..46415e33 --- /dev/null +++ b/src/components/Footer/Footer.js @@ -0,0 +1,22 @@ +/* eslint-disable prettier/prettier */ +import React from 'react' + +export default class Footer extends React.Component { + + constructor(props) { + super(props) + this.state = {} + } + + + render() { + const {children} = this.props + return ( + <> + <footer data-testid="app-footer"> + {children} + </footer> + </> + ) + } +} \ No newline at end of file diff --git a/src/components/Footer/index.js b/src/components/Footer/index.js new file mode 100644 index 00000000..3738288b --- /dev/null +++ b/src/components/Footer/index.js @@ -0,0 +1 @@ +export { default } from "./Footer"; diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js new file mode 100644 index 00000000..2a389605 --- /dev/null +++ b/src/components/Header/Header.js @@ -0,0 +1,14 @@ +/* eslint-disable prettier/prettier */ +import React from "react"; + +export default class Header extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { children } = this.props; + return <>{children}</>; + } +} diff --git a/src/components/Header/index.js b/src/components/Header/index.js new file mode 100644 index 00000000..2764567d --- /dev/null +++ b/src/components/Header/index.js @@ -0,0 +1 @@ +export { default } from "./Header"; diff --git a/src/components/MainList/MainList.js b/src/components/MainList/MainList.js new file mode 100644 index 00000000..aeba7a7f --- /dev/null +++ b/src/components/MainList/MainList.js @@ -0,0 +1,34 @@ +/* eslint-disable prettier/prettier */ +import React from 'react' +import { Switch, Route } from "react-router-dom"; +import Home from "../../pages/Home" +import Active from "../../pages/Active" +import Completed from "../../pages/Completed" + +export default class MainList extends React.Component { + + constructor(props) { + super(props) + this.state = { + + } + } + + render() { + return ( + <> + <Switch> + <Route exact path="/"> + <Home {...this.props} /> + </Route> + <Route exact path="/completed"> + <Completed {...this.props} /> + </Route> + <Route exact path="/active"> + <Active {...this.props} /> + </Route> + </Switch> + </> + ) + } +} \ No newline at end of file diff --git a/src/components/MainList/index.js b/src/components/MainList/index.js new file mode 100644 index 00000000..f03c1bec --- /dev/null +++ b/src/components/MainList/index.js @@ -0,0 +1 @@ +export { default } from "./MainList"; diff --git a/src/components/Title/Index.js b/src/components/Title/Index.js new file mode 100644 index 00000000..17d11b0c --- /dev/null +++ b/src/components/Title/Index.js @@ -0,0 +1,3 @@ +import Title from "./Title"; + +export default Title; diff --git a/src/components/Title/Title.js b/src/components/Title/Title.js new file mode 100644 index 00000000..eae7e6f6 --- /dev/null +++ b/src/components/Title/Title.js @@ -0,0 +1,19 @@ +/* eslint-disable prettier/prettier */ +import React from "react"; + +import "./Title.scss"; + +export default class Title extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + return ( + <h1 className="title display-1 text-white text-center m-5 font-weight-bold"> + <strong>To Do List</strong> + </h1> + ); + } +} diff --git a/src/components/Title/Title.scss b/src/components/Title/Title.scss new file mode 100644 index 00000000..70695b6a --- /dev/null +++ b/src/components/Title/Title.scss @@ -0,0 +1,3 @@ +.title{ + text-shadow: 1px 1px 2px black, 0 0 1em white, 0 0 0.2em white; +} \ No newline at end of file diff --git a/src/components/TodoList/TodoList.js b/src/components/TodoList/TodoList.js new file mode 100644 index 00000000..7ef2f6ec --- /dev/null +++ b/src/components/TodoList/TodoList.js @@ -0,0 +1,34 @@ +/* eslint-disable prettier/prettier */ +import React from 'react' +// Improve the render of the component +import TodoTask from "../TodoTask" + +export default class TodoList extends React.Component { + + constructor(props) { + super(props) + + this.state = {} + } + + render() { + const {tasks} = this.props + + return ( + <> + { + tasks.length === 0 ? + <p className="text-center mt-3 h3" data-testid="no-todos">There is any to do yet <span role="img" aria-label="sad face">😥</span></p> : + <ul className="list-group" data-testid="todos-list"> + { + tasks.map(task => ( + <TodoTask key={task.id} task={task} {...this.props} /> + )) + } + </ul> + + } + </> + ) + } +} \ No newline at end of file diff --git a/src/components/TodoList/index.js b/src/components/TodoList/index.js new file mode 100644 index 00000000..b219c99e --- /dev/null +++ b/src/components/TodoList/index.js @@ -0,0 +1,2 @@ +/* eslint-disable prettier/prettier */ +export { default } from "./TodoList" \ No newline at end of file diff --git a/src/components/TodoTask/TodoTask.js b/src/components/TodoTask/TodoTask.js new file mode 100644 index 00000000..70938edc --- /dev/null +++ b/src/components/TodoTask/TodoTask.js @@ -0,0 +1,101 @@ +/* eslint-disable prettier/prettier */ +import React from "react"; +import { saveItem } from "../../utils/localStorage"; + +import "./TodoTask.scss"; + +export default class TodoTask extends React.Component { + constructor(props) { + super(props); + + const { task } = this.props; + + this.state = { + id: task.id, + done: task.done, + isEditing: task.isEditing, + inputValue: task.inputValue, + }; + } + + completeHandler = () => { + const { done } = this.state; + const { refreshState } = this.props; + !done + ? saveItem({ ...this.state, done: true }, this.setState({ done: true })) + : saveItem( + { ...this.state, done: false }, + this.setState({ done: false }), + ); + refreshState(); + }; + + handlerSubmit = (event) => { + const { refreshState } = this.props; + event.preventDefault(); + event.target.firstChild.blur(); + saveItem({ ...this.state, isEditing: false }); + refreshState(); + }; + + editHandler = (event) => { + this.setState({ inputValue: event.target.value }); + }; + + render() { + const { inputValue, id, isEditing, done } = this.state; + const { handlerDeleteTask } = this.props; + + if (id === "") return null; + + return ( + <li + className={`shadow-sm list-group-item d-flex flex-wrap justify-content-between align-items-center ${ + isEditing ? "editing" : "" + } ${done ? "done" : ""}`} + data-testid="todo-item" + > + <button + className="btn btn-light" + type="button" + onClick={this.completeHandler} + data-testid="todo-item-checkbox" + > + <span role="img" aria-label="Check to complete"> + {done ? "✅" : "⬜"} + </span> + </button> + <form onSubmit={this.handlerSubmit}> + <input + type="checkbox" + className="d-none" + defaultChecked={done ? `checked` : ``} + /> + <input + required + title={inputValue} + className="input-task" + type="text" + key={id} + id={id} + value={inputValue} + onChange={this.editHandler} + onFocus={() => this.setState({ isEditing: true })} + onBlur={() => this.setState({ isEditing: false })} + data-testid="todo-item-input" + /> + </form> + <button + className="btn btn-light" + type="button" + onClick={() => handlerDeleteTask(id)} + data-testid="todo-item-delete-button" + > + <span role="img" aria-label="trash bin"> + 🗑️ + </span> + </button> + </li> + ); + } +} diff --git a/src/components/TodoTask/TodoTask.scss b/src/components/TodoTask/TodoTask.scss new file mode 100644 index 00000000..aec2a861 --- /dev/null +++ b/src/components/TodoTask/TodoTask.scss @@ -0,0 +1,26 @@ +.list-group-item+.list-group-item, +.list-group-item:first-child { + border-top-width: 1px; + margin-bottom: 7.5px; +} +.done { + + border-color: green; + opacity: 0.75; + + .input-task { + text-decoration: line-through; + } + +} + +.input-task { + border: none; + width: 100%; + text-overflow: ellipsis; + outline: none; + &:focus{ + border-bottom: solid 1px; + } + +} \ No newline at end of file diff --git a/src/components/TodoTask/index.js b/src/components/TodoTask/index.js new file mode 100644 index 00000000..686c87c9 --- /dev/null +++ b/src/components/TodoTask/index.js @@ -0,0 +1 @@ +export { default } from "./TodoTask"; diff --git a/src/img/ToDoPlanning.png b/src/img/ToDoPlanning.png new file mode 100644 index 00000000..9055d261 Binary files /dev/null and b/src/img/ToDoPlanning.png differ diff --git a/src/img/runny-run-homegiffy.gif b/src/img/runny-run-homegiffy.gif new file mode 100644 index 00000000..c23a3e08 Binary files /dev/null and b/src/img/runny-run-homegiffy.gif differ diff --git a/src/index.js b/src/index.js index 19bb154c..5516f597 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import React from "react"; import ReactDOM from "react-dom"; import "bootstrap/dist/css/bootstrap.min.css"; +import "./index.scss"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; diff --git a/src/index.scss b/src/index.scss new file mode 100644 index 00000000..4b28be43 --- /dev/null +++ b/src/index.scss @@ -0,0 +1,4 @@ +main{ + position: relative; + top: -40vh; +} diff --git a/src/pages/Active/Active.js b/src/pages/Active/Active.js new file mode 100644 index 00000000..21f1cb59 --- /dev/null +++ b/src/pages/Active/Active.js @@ -0,0 +1,24 @@ +/* eslint-disable prettier/prettier */ +import React from "react"; +import TodoList from "../../components/TodoList"; + +export default class Active extends React.Component { + + constructor(props) { + super(props) + this.state = {} + } + + + render() { + + const {tasks} = this.props + const activeTasks = tasks.filter(task => !task.done) + + return ( + <> + <TodoList {...this.props} tasks={activeTasks} /> + </> + ) + } +} \ No newline at end of file diff --git a/src/pages/Active/index.js b/src/pages/Active/index.js new file mode 100644 index 00000000..bfae90d4 --- /dev/null +++ b/src/pages/Active/index.js @@ -0,0 +1 @@ +export { default } from "./Active"; diff --git a/src/pages/Completed/Completed.js b/src/pages/Completed/Completed.js new file mode 100644 index 00000000..538072ac --- /dev/null +++ b/src/pages/Completed/Completed.js @@ -0,0 +1,21 @@ +/* eslint-disable prettier/prettier */ +import React from "react"; +import TodoList from "../../components/TodoList"; + +export default class Completed extends React.Component { + constructor(props) { + super(props); + this.state = {}; + } + + render() { + const { tasks } = this.props; + const completedTasks = tasks.filter((task) => task.done); + + return ( + <> + <TodoList {...this.props} tasks={completedTasks} /> + </> + ); + } +} diff --git a/src/pages/Completed/index.js b/src/pages/Completed/index.js new file mode 100644 index 00000000..2472503b --- /dev/null +++ b/src/pages/Completed/index.js @@ -0,0 +1 @@ +export { default } from "./Completed"; diff --git a/src/pages/Home/Home.js b/src/pages/Home/Home.js new file mode 100644 index 00000000..3620b49b --- /dev/null +++ b/src/pages/Home/Home.js @@ -0,0 +1,20 @@ +/* eslint-disable prettier/prettier */ +import React from "react"; +import TodoList from "../../components/TodoList"; + +export default class Home extends React.Component { + + constructor(props) { + super(props) + this.state = {} + } + + + render() { + return ( + <> + <TodoList {...this.props} /> + </> + ) + } +} \ No newline at end of file diff --git a/src/pages/Home/index.js b/src/pages/Home/index.js new file mode 100644 index 00000000..ffa79319 --- /dev/null +++ b/src/pages/Home/index.js @@ -0,0 +1 @@ +export { default } from "./Home"; diff --git a/src/utils/localStorage.js b/src/utils/localStorage.js new file mode 100644 index 00000000..46287c0f --- /dev/null +++ b/src/utils/localStorage.js @@ -0,0 +1,16 @@ +/* eslint-disable prettier/prettier */ +export function saveItem(item) { + window.localStorage.setItem(item.id, JSON.stringify(item)) +} + +export function getItem(id) { + return window.localStorage.getItem(id) +} + +export function deleteItem(id) { + window.localStorage.removeItem(id) +} + +export function generateNewKey() { + return Object.keys(localStorage).length > 0 ? Math.max(...Object.keys(localStorage)) + 1 : 1 +} \ No newline at end of file diff --git a/src/utils/updateState.js b/src/utils/updateState.js new file mode 100644 index 00000000..68225997 --- /dev/null +++ b/src/utils/updateState.js @@ -0,0 +1,15 @@ +/* eslint-disable prettier/prettier */ + +// Reusable function to update state from component +/* +import {useState} from "react" + +export default function updateState(event) { + + const [state, setState] = useState() + + setState(() => { + return event.target.value + }) +} +*/ \ No newline at end of file