├── public ├── favicon.png ├── manifest.json └── index.html ├── src ├── assets │ ├── desktop-preview.jpg │ └── images │ │ ├── bg-desktop-dark.jpg │ │ ├── bg-mobile-dark.jpg │ │ ├── bg-mobile-light.jpg │ │ ├── bg-desktop-light.jpg │ │ ├── icon-check.svg │ │ ├── icon-cross.svg │ │ ├── icon-moon.svg │ │ └── icon-sun.svg ├── index.js ├── styles │ ├── _mixins.scss │ ├── _vars.scss │ ├── _themes.scss │ └── App.scss ├── components │ ├── Header.js │ ├── Form.js │ ├── Filter.js │ ├── TodoItem.js │ └── TodoList.js ├── data │ └── data.json └── App.js ├── .gitignore ├── package.json └── README.md /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mileine/frontendmentor-todo-app-react/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/assets/desktop-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mileine/frontendmentor-todo-app-react/HEAD/src/assets/desktop-preview.jpg -------------------------------------------------------------------------------- /src/assets/images/bg-desktop-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mileine/frontendmentor-todo-app-react/HEAD/src/assets/images/bg-desktop-dark.jpg -------------------------------------------------------------------------------- /src/assets/images/bg-mobile-dark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mileine/frontendmentor-todo-app-react/HEAD/src/assets/images/bg-mobile-dark.jpg -------------------------------------------------------------------------------- /src/assets/images/bg-mobile-light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mileine/frontendmentor-todo-app-react/HEAD/src/assets/images/bg-mobile-light.jpg -------------------------------------------------------------------------------- /src/assets/images/bg-desktop-light.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mileine/frontendmentor-todo-app-react/HEAD/src/assets/images/bg-desktop-light.jpg -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.querySelector('#root')); -------------------------------------------------------------------------------- /src/assets/images/icon-check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | $mobile-width: 580px; 2 | 3 | @mixin mobile { 4 | @media (max-width: #{$mobile-width}) { 5 | @content 6 | } 7 | }; 8 | 9 | @mixin tablet { 10 | @media (min-width: #{$mobile-width + 1}) { 11 | @content 12 | } 13 | } -------------------------------------------------------------------------------- /src/assets/images/icon-cross.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/icon-moon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .eslintcache 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ReactComponent as LightIcon} from '../assets/images/icon-sun.svg' 3 | import { ReactComponent as DarkIcon} from '../assets/images/icon-moon.svg' 4 | 5 | const Header = ({ theme, toggleTheme }) => { 6 | return ( 7 |
8 |

TODO

9 | 12 |
13 | ) 14 | } 15 | 16 | export default Header -------------------------------------------------------------------------------- /src/assets/images/icon-sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/data/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "text": "Complete online JavaScript course", 5 | "checked": true 6 | }, 7 | { 8 | "id": 2, 9 | "text": "Jog around the park 3x", 10 | "checked": false 11 | }, 12 | { 13 | "id": 3, 14 | "text": "10 minutes meditation", 15 | "checked": false 16 | }, 17 | { 18 | "id": 4, 19 | "text": "Read for 1 hour", 20 | "checked": false 21 | }, 22 | { 23 | "id": 5, 24 | "text": "Pick up groceries", 25 | "checked": false 26 | }, 27 | { 28 | "id": 6, 29 | "text": "Complete Todo App Frontend Mentor", 30 | "checked": false 31 | } 32 | ] -------------------------------------------------------------------------------- /src/styles/_vars.scss: -------------------------------------------------------------------------------- 1 | $bright-blue: hsl(220, 98%, 61%); 2 | $check-background: linear-gradient(135deg, hsl(192, 100%, 67%),hsl(280, 87%, 65%)); 3 | 4 | /* Light Theme */ 5 | 6 | $very-light-gray: hsl(0, 0%, 98%); 7 | $very-light-grayish-blue: hsl(236, 33%, 92%); 8 | $light-grayish-blue: hsl(233, 11%, 84%); 9 | $dark-grayish-blue: hsl(236, 9%, 61%); 10 | $very-dark-grayish-blue: hsl(235, 19%, 35%); 11 | 12 | /* Dark Theme */ 13 | 14 | $very-dark-blue: hsl(235, 21%, 11%); 15 | $very-dark-desaturated-blue: hsl(235, 24%, 19%); 16 | $light-grayish-blue: hsl(234, 39%, 85%); 17 | $light-grayish-blue-hover: hsl(236, 33%, 92%); 18 | $dark-grayish-blue: hsl(234, 11%, 52%); 19 | $very-dark-grayish-blue: hsl(233, 14%, 35%); 20 | $very-dark-grayish-blue-border: hsl(237, 14%, 26%); 21 | -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | const Form = ({ nextId, addItem }) => { 4 | const [text, setText] = useState('') 5 | const onSubmit = (evt) => { 6 | evt.preventDefault() 7 | const newValue = { 8 | "id": nextId, 9 | "text": text, 10 | "checked": false 11 | } 12 | addItem(newValue) 13 | setText('') 14 | } 15 | 16 | return ( 17 |
18 |
19 | 20 | setText(evt.target.value) }/> 21 |
22 |
23 | ) 24 | } 25 | 26 | export default Form -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "node-sass": "4.14.1", 10 | "react": "^17.0.1", 11 | "react-beautiful-dnd": "^13.0.0", 12 | "react-dom": "^17.0.1", 13 | "react-scripts": "4.0.1", 14 | "web-vitals": "^0.2.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/Filter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Filter = ({ selectedFilter, updateFilter }) => { 4 | const handleClick = (selected) => { 5 | 6 | updateFilter(selected) 7 | } 8 | return( 9 |
10 | 18 | 26 | 34 |
35 | ) 36 | } 37 | 38 | export default Filter -------------------------------------------------------------------------------- /src/components/TodoItem.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { ReactComponent as CompleteIcon } from '../assets/images/icon-cross.svg' 3 | import { Draggable } from 'react-beautiful-dnd' 4 | 5 | const TodoItem = ({ index, id, checked, updateItem, deleteItem, text }) => { 6 | const [completed, setCompleted] = useState(checked) 7 | 8 | const updateItemStatus = (id) => { 9 | updateItem(id) 10 | setCompleted(!completed) 11 | } 12 | 13 | const onKeyPress = (event, id) => { 14 | if (event.key === 'Enter') updateItem(id) 15 | setCompleted(!completed) 16 | } 17 | 18 | return ( 19 | 20 | {(provided) => ( 21 |
  • onKeyPress(event, id)} 28 | > 29 |
    30 |
    updateItemStatus(id)}> 31 |
    32 |
    33 | updateItemStatus(id)}>{text} 34 |
    35 | 38 |
  • 39 | )} 40 |
    41 | ) 42 | } 43 | 44 | export default TodoItem -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Todo App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/styles/_themes.scss: -------------------------------------------------------------------------------- 1 | @import 'vars'; 2 | 3 | /* theme settings */ 4 | $themes: ( 5 | "light": ( 6 | "color-bg": #fff, 7 | "color-list-bg": #fff, 8 | "color-list-text": $dark-grayish-blue, 9 | "color-text-light": $dark-grayish-blue, 10 | "color-list-text-completed": $light-grayish-blue, 11 | "color-input-placeholder": $dark-grayish-blue, 12 | "color-input": $very-dark-grayish-blue, 13 | "color-path": $very-dark-grayish-blue, 14 | "color-btn":$dark-grayish-blue, 15 | "color-btn-hover": $very-dark-grayish-blue, 16 | "color-btn-active": $bright-blue, 17 | "border": 1px solid hsl(236, 33%, 92%), 18 | "shadow": 0px 35px 50px #C2C3D680, 19 | ), 20 | "dark": ( 21 | "color-bg": $very-dark-blue, 22 | "color-list-bg": $very-dark-desaturated-blue, 23 | "color-list-text": $light-grayish-blue, 24 | "color-text-light": $very-dark-grayish-blue, 25 | "color-list-text-completed": $very-dark-grayish-blue, 26 | "color-input-placeholder": $very-dark-grayish-blue, 27 | "color-input": $light-grayish-blue-hover, 28 | "color-path": $light-grayish-blue-hover, 29 | "color-btn": $dark-grayish-blue, 30 | "color-btn-hover": $light-grayish-blue-hover, 31 | "color-btn-active":$bright-blue, 32 | "border": 1px solid hsl(237, 14%, 26%), 33 | "shadow": 0px 35px 50px #00000080, 34 | ) 35 | ); 36 | 37 | @mixin apply-theme-color($key, $color) { 38 | @each $theme-name, $theme-color in $themes { 39 | .#{$theme-name} & { 40 | #{$key}: map-get(map-get($themes, $theme-name), $color ) 41 | } 42 | } 43 | } 44 | 45 | $theme-list: light dark; 46 | 47 | @mixin backgroundImg { 48 | @each $theme in $theme-list { 49 | .#{$theme} & { 50 | background: url('/assets/images/bg-desktop-'+$theme+'.jpg'); 51 | background-size: 100% auto; 52 | background-repeat: no-repeat; 53 | @include mobile { 54 | background: url('/assets/images/bg-mobile-'+$theme+'.jpg'); 55 | background-size: 100% auto; 56 | background-repeat: no-repeat; 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frontend Mentor - Todo App 2 | 3 | This project is a challenge by [Frontend Mentor](https://www.frontendmentor.io). 4 | 5 | 6 | ![Todo App](https://raw.githubusercontent.com/mileine/frontendmentor-todo-app-react/main/src/assets/desktop-preview.jpg "Todo App - Design preview") 7 | 8 | 9 | ## Challenge description 10 | 11 | Users should be able to: 12 | 13 | - View the optimal layout for the app depending on their device's screen size 14 | - See hover states for all interactive elements on the page 15 | - Add new todos to the list 16 | - Mark todos as complete 17 | - Delete todos from the list 18 | - Filter by all/active/complete todos 19 | - Clear all completed todos 20 | - Toggle light and dark mode 21 | - Bonus: Drag and drop to reorder items on the list 22 | 23 | View the full description [here](https://www.frontendmentor.io/challenges/todo-app-Su1_KokOW). 24 | 25 | ## About the app 26 | 27 | ### Features 28 | 29 | All the functionalities described on *Challenge description* section were implemented. 30 | 31 | The additional behaviours below were also implemented: 32 | 33 | - All the basic functionalities are accessible on the keyboard 34 | - The task lists is saved on localstorage 35 | 36 | ### Tools 37 | 38 | - ReactJS (Create React App) 39 | - React Beautiful DnD 40 | - SASS 41 | 42 | 43 | ### Known issues 44 | 45 | The [report](https://www.frontendmentor.io/solutions/todo-app-using-reactjs-I9UzE1vG2/report) generated on Frontend Mentor website lists 15 HTML issues that were not fixed. 46 | 47 | 48 | ## How to run 49 | 50 | 1. Clone this repo. 51 | 52 | 2. Run `yarn install`. 53 | 54 | 3. Run `yarn start`. 55 | 56 | 57 | ## Demo 58 | 59 | View [demo](https://frontendmentor-todo-app-react.vercel.app/). 60 | 61 | ## References 62 | 63 | Here's a list of useful resources I learned a lot from when working on this challenge: 64 | 65 | - [Gradient Borders in CSS](https://css-tricks.com/gradient-borders-in-css/) 66 | - [How I Theme My React App With Sass](https://www.webtips.dev/how-i-theme-my-react-app-with-sass) 67 | - [Sass Essential Training](https://www.linkedin.com/learning-login/share?forceAccount=false&redirect=https%3A%2F%2Fwww.linkedin.com%2Flearning%2Fsass-essential-training%3Ftrk%3Dshare_ent_url) 68 | - [Sass mixin to have multiple theme based background images](https://medium.com/@traumastronaut/sass-mixin-to-have-multiple-theme-based-background-images-e278439299e6) 69 | - [Session Storage and Local Storage in React](https://www.robinwieruch.de/local-storage-react) 70 | - [How to Add Drag and Drop in React with React Beautiful DnD](https://www.freecodecamp.org/news/how-to-add-drag-and-drop-in-react-with-react-beautiful-dnd/) -------------------------------------------------------------------------------- /src/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Filter from './Filter' 3 | import TodoItem from './TodoItem' 4 | import { DragDropContext, Droppable } from 'react-beautiful-dnd' 5 | 6 | const TodoList = ({ items, updateItem, selectedFilter, updateFilter, updateItems, deleteItem, clearCompleted }) => { 7 | 8 | const renderedList = items.map(({id, text, checked}, index) => { 9 | return ( 10 | 11 | ) 12 | }) 13 | 14 | const renderedListActive = items.map(({id, text, checked}, index) => { 15 | if (checked) return '' 16 | return ( 17 | 18 | ) 19 | }) 20 | 21 | const renderedListCompleted = items.map(({id, text, checked}, index) => { 22 | if (!checked) return '' 23 | return ( 24 | 25 | ) 26 | }) 27 | 28 | const renderedFilteredList = () => { 29 | if (selectedFilter === 'active') return renderedListActive 30 | else if (selectedFilter === 'completed') return renderedListCompleted 31 | else return renderedList 32 | } 33 | 34 | const getItemsLeft = () => { 35 | let completedItems = 0 36 | let total = items.length 37 | completedItems = items.filter(item => item.checked).length 38 | return total - completedItems 39 | } 40 | 41 | const handleOnDragEnd = (result) => { 42 | if (!result.destination) return; 43 | const updatedList = Array.from(items) 44 | const [reorderedItem] = updatedList.splice(result.source.index, 1) 45 | updatedList.splice(result.destination.index, 0, reorderedItem) 46 | updateItems(updatedList) 47 | } 48 | 49 | return ( 50 |
    51 | 52 | 53 | {(provided) => ( 54 |
      55 | { renderedFilteredList() } 56 | {provided.placeholder} 57 |
    58 | )} 59 |
    60 |
    61 |
    62 | { getItemsLeft() } items left 63 | 64 | 65 |
    66 |
    67 | ) 68 | } 69 | 70 | export default TodoList -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import './styles/App.scss'; 3 | import Header from './components/Header' 4 | import Form from './components/Form' 5 | import Filter from './components/Filter' 6 | import TodoList from './components/TodoList'; 7 | import todoList from './data/data.json' 8 | 9 | const App = () => { 10 | const MY_TASKS = localStorage.getItem('myTasks') ? JSON.parse(localStorage.getItem('myTasks')) : [...todoList] 11 | const [theme, setTheme] = useState('light') 12 | const [items, setItems] = useState(MY_TASKS) 13 | const [selectedFilter, setSelectedFilter] = useState('all') 14 | const [nextId, setNextId] = useState(items.length + 2) 15 | 16 | useEffect(() => { 17 | document.body.className = theme 18 | localStorage.setItem('myTasks', JSON.stringify(items)) 19 | }, [theme, items]) 20 | 21 | const toggleTheme = () => { 22 | setTheme(theme === 'light'? 'dark': 'light') 23 | } 24 | 25 | const addItem = (newItem) => { 26 | setItems(items => [...items, newItem]) 27 | setNextId(nextId + 1) 28 | } 29 | 30 | const updateItem = (id) => { 31 | const newList = [...items] 32 | 33 | const editItem = newList.find((item => item.id === id)) 34 | 35 | if (editItem) { 36 | editItem.checked = !editItem.checked 37 | } 38 | 39 | setItems(newList) 40 | } 41 | 42 | const updateFilter = (selected) => { 43 | setSelectedFilter(selected) 44 | } 45 | 46 | const updateItems = (updatedList) => { 47 | setItems(updatedList) 48 | } 49 | 50 | const deleteItem = (id) => { 51 | const newList = [...items] 52 | setItems(newList.filter(item => item.id !== id)) 53 | } 54 | 55 | const clearCompleted = () => { 56 | const itemsArray = [...items] 57 | setItems(itemsArray.filter(item => !item.checked)) 58 | } 59 | 60 | return ( 61 |
    62 |
    63 |
    64 |
    65 | 66 |
    67 | 68 |
    69 | Drag and drop to reorder list 70 |
    71 |
    72 |

    Challenge by Frontend Mentor. 73 | Coded with lots of❤️ by Mi Souto.

    74 |
    75 |
    76 | ); 77 | } 78 | 79 | export default App 80 | -------------------------------------------------------------------------------- /src/styles/App.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Josefin+Sans:wght@400;700&display=swap'); 2 | @import 'vars'; 3 | @import 'mixins'; 4 | @import 'themes'; 5 | 6 | * { 7 | padding: 0; 8 | margin: 0; 9 | box-sizing: border-box; 10 | font-family: 'Josefin Sans', sans-serif; 11 | } 12 | 13 | ul { 14 | list-style: none; 15 | } 16 | 17 | button, li { 18 | outline-color: $bright-blue; 19 | } 20 | 21 | /* LAYOUT 22 | --------------------*/ 23 | .container { 24 | min-height: 100vh; 25 | width: 100vw; 26 | @include backgroundImg; 27 | @include apply-theme-color('background-color', 'color-bg'); 28 | } 29 | 30 | .main { 31 | max-width: 540px; 32 | margin: auto; 33 | display: flex; 34 | flex-direction: column; 35 | @include mobile { 36 | max-width: 90%; 37 | } 38 | } 39 | 40 | /* HEADER 41 | --------------------*/ 42 | 43 | .header { 44 | color: #fff; 45 | display: flex; 46 | justify-content: space-between; 47 | margin-top: 70px; 48 | margin-bottom: 50px; 49 | .title { 50 | letter-spacing: 15px; 51 | font-size: 40px; 52 | font-weight: 700; 53 | } 54 | } 55 | 56 | .btn-switch { 57 | background-color: transparent; 58 | cursor: pointer; 59 | border: none; 60 | width: 25px; 61 | height: 25px; 62 | } 63 | 64 | .attribution { 65 | position: fixed; 66 | padding: 10px; 67 | text-align: center; 68 | font-size: 12px; 69 | width: 100%; 70 | bottom: 0; 71 | display: flex; 72 | justify-content: center; 73 | box-shadow: 0px -5px 5px rgba(68, 68, 68, 0.1); 74 | @include apply-theme-color('background-color', 'color-bg'); 75 | @include apply-theme-color('color', 'color-list-text'); 76 | a { 77 | color: $bright-blue; 78 | } 79 | > p { 80 | letter-spacing: .4px; 81 | opacity: 1; 82 | } 83 | .love { 84 | margin: 0 5px; 85 | } 86 | } 87 | 88 | /* Form 89 | --------------------*/ 90 | 91 | .input-wrapper { 92 | width: 100%; 93 | display: flex; 94 | align-items: center; 95 | padding: 20px; 96 | border-radius: 5px; 97 | @include apply-theme-color('background', 'color-list-bg'); 98 | @include apply-theme-color('box-shadow', 'shadow'); 99 | input { 100 | &:focus { 101 | @include apply-theme-color('color', 'color-input'); 102 | } 103 | &::placeholder { 104 | @include apply-theme-color('color', 'color-input-placeholder'); 105 | } 106 | } 107 | } 108 | 109 | input { 110 | font-size: 18px; 111 | letter-spacing: -0.25px; 112 | background-color: transparent; 113 | border-color: transparent; 114 | width: 100%; 115 | position: relative; 116 | &::placeholder { 117 | color: #9495A5; 118 | } 119 | } 120 | 121 | input:focus { 122 | outline: pink; 123 | } 124 | 125 | .input-check { 126 | outline: none; 127 | display: inline-block; 128 | border-radius: 50%; 129 | height: 24px; 130 | width: 24px; 131 | margin-right: 10px; 132 | background: transparent; 133 | @include apply-theme-color('border', 'border'); 134 | } 135 | 136 | /* LIST 137 | --------------------*/ 138 | .todo-list-wrapper { 139 | display: flex; 140 | flex-direction: column; 141 | justify-content: space-between; 142 | max-width: 540px; 143 | margin-top: 25px; 144 | border-radius: 5px; 145 | min-height: 300px; 146 | @include apply-theme-color('background', 'color-list-bg'); 147 | @include apply-theme-color('box-shadow', 'shadow'); 148 | } 149 | 150 | .todo-item{ 151 | position: relative; 152 | cursor: pointer; 153 | width: 100%; 154 | display: flex; 155 | align-items: center; 156 | justify-content: space-between; 157 | padding: 20px; 158 | font-size: 18px; 159 | letter-spacing: -0.25px; 160 | @include apply-theme-color('color', 'color-list-text'); 161 | @include apply-theme-color('border-bottom', 'border'); 162 | &.completed { 163 | @include apply-theme-color('color', 'color-list-text-completed'); 164 | text-decoration: line-through; 165 | } 166 | &:first-of-type { 167 | border-top-left-radius: 5px; 168 | border-top-right-radius: 5px; 169 | } 170 | .description { 171 | display: flex; 172 | align-items: center; 173 | } 174 | .item-check-wrapper { 175 | display: flex; 176 | align-items: center; 177 | justify-content: center; 178 | width: 24px; 179 | height: 24px; 180 | border-radius: 50%; 181 | margin-right: 10px; 182 | } 183 | .item-check { 184 | display: inline-block; 185 | border-radius: 50%; 186 | height: 24px; 187 | width: 24px; 188 | background: transparent; 189 | &.completed { 190 | background: url('../assets/images/icon-check.svg'), linear-gradient(135deg, hsl(192, 100%, 67%),hsl(280, 87%, 65%)); 191 | background-repeat: no-repeat; 192 | background-position: center; 193 | } 194 | } 195 | .delete-icon { 196 | transition: opacity 0.5s ease-in-out; 197 | background: transparent; 198 | border: none; 199 | opacity: 0; 200 | cursor: pointer; 201 | & > path { 202 | @include apply-theme-color('fill', 'color-path'); 203 | } 204 | &:focus { 205 | opacity: 1; 206 | } 207 | } 208 | &:not(:hover) { 209 | .item-check:not(.completed) { 210 | @include apply-theme-color('border', 'border'); 211 | } 212 | } 213 | &:hover, 214 | &:focus { 215 | .delete-icon{ 216 | opacity: 1; 217 | } 218 | .item-check-wrapper { 219 | background: linear-gradient(135deg, hsl(192, 100%, 67%),hsl(280, 87%, 65%)); 220 | .item-check { 221 | &:not(.completed) { 222 | width: 22px; 223 | height: 22px; 224 | @include apply-theme-color('background', 'color-list-bg'); 225 | } 226 | } 227 | } 228 | } 229 | } 230 | 231 | .todo-list-footer { 232 | padding: 20px; 233 | display: flex; 234 | justify-content: space-between; 235 | align-items: center; 236 | .items-left { 237 | display: block; 238 | width: 100px; 239 | @include apply-theme-color('color', 'color-btn'); 240 | font-size: 14px; 241 | letter-spacing: -0.25px; 242 | } 243 | .filter-wrapper { 244 | @include mobile { 245 | display: none; 246 | } 247 | } 248 | } 249 | 250 | .btn-clear { 251 | background: transparent; 252 | border: none; 253 | cursor: pointer; 254 | padding: 10px; 255 | @include apply-theme-color('color', 'color-btn'); 256 | &:focus { 257 | border: none; 258 | } 259 | &:hover { 260 | @include apply-theme-color('color', 'color-btn-hover'); 261 | } 262 | } 263 | 264 | /* FILTER 265 | --------------------*/ 266 | .filter-wrapper { 267 | display: flex; 268 | width: 166px; 269 | justify-content: space-between; 270 | .filter-option{ 271 | cursor: pointer; 272 | font-size: 14px; 273 | font-weight: bold; 274 | background: transparent; 275 | border: none; 276 | padding: 10px; 277 | @include apply-theme-color('color', 'color-text-light'); 278 | &:focus { 279 | border: none; 280 | } 281 | &:hover { 282 | @include apply-theme-color('color', 'color-btn-hover'); 283 | } 284 | &.selected { 285 | color: #3A7CFD; 286 | } 287 | } 288 | @include mobile { 289 | width: 100%; 290 | padding: 20px; 291 | margin-top: 15px; 292 | border-radius: 5px; 293 | .filter-option { 294 | margin-right: 20px; 295 | } 296 | justify-content: center; 297 | @include apply-theme-color('background', 'color-list-bg'); 298 | @include apply-theme-color('box-shadow', 'shadow'); 299 | } 300 | } 301 | 302 | .filter-mobile { 303 | @include tablet { 304 | display: none; 305 | } 306 | } 307 | 308 | /* INFO 309 | --------------------*/ 310 | .dnd-info { 311 | display: block; 312 | margin: 40px auto 80px auto; 313 | @include apply-theme-color('color', 'color-text-light'); 314 | } --------------------------------------------------------------------------------