├── public ├── robots.txt ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── manifest.json └── index.html ├── src ├── index.js ├── Alert.js ├── DarkModeToggle.js ├── DarkModeToggle.scss ├── List.js ├── Colors.js ├── Task.js ├── context.js ├── App.js └── index.css ├── .gitignore ├── package.json ├── LICENSE.md └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyasbelaoud/react-todo-app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyasbelaoud/react-todo-app/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyasbelaoud/react-todo-app/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyasbelaoud/react-todo-app/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyasbelaoud/react-todo-app/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilyasbelaoud/react-todo-app/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import { AppProvider } from "./context"; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ); 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "To-Do List", 3 | "name": "To-Do List", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "android-chrome-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "android-chrome-512x512.png", 17 | "sizes": "512x512", 18 | "type": "image/png" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | To-Do List 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Alert.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useGlobalContext } from "./context"; 3 | 4 | const Alert = ({ msg }) => { 5 | const { tasks, refContainer, alert, showAlert } = useGlobalContext(); 6 | 7 | useEffect(() => { 8 | refContainer.current.style.left = `${alert.show ? "15px" : "-100%"}`; 9 | 10 | const timeout = setTimeout(() => { 11 | refContainer.current.style.left = "-100%"; 12 | showAlert(false, alert.msg); 13 | }, 4000); 14 | 15 | return () => clearTimeout(timeout); 16 | }, [alert, refContainer, showAlert, tasks]); 17 | return ( 18 |

19 | {msg} 20 |

21 | ); 22 | }; 23 | 24 | export default Alert; 25 | -------------------------------------------------------------------------------- /src/DarkModeToggle.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import './DarkModeToggle.scss'; 3 | 4 | const getUserTheme = () => { 5 | const theme = localStorage.getItem('theme') || 'dark'; 6 | return theme === 'dark' ? true : false; 7 | }; 8 | 9 | const DarkModeToggle = () => { 10 | const [isDarkMode, setIsDarkMode] = useState(getUserTheme()); 11 | 12 | useEffect(() => { 13 | document.documentElement.className = `${isDarkMode && 'dark'}`; 14 | localStorage.setItem('theme', isDarkMode ? 'dark' : 'light'); 15 | }, [isDarkMode]) 16 | 17 | return ( 18 |
19 | setIsDarkMode(e.target.checked)}/> 20 | 21 |
22 | ) 23 | } 24 | 25 | export default DarkModeToggle -------------------------------------------------------------------------------- /src/DarkModeToggle.scss: -------------------------------------------------------------------------------- 1 | .toggle-btn { 2 | position: absolute; 3 | top: 0; 4 | right: 20px; 5 | 6 | input[type="checkbox"] { 7 | visibility: hidden; 8 | &:checked + label { 9 | transform: rotate(360deg); 10 | background-color: #111; 11 | &:before { 12 | transform: translateX(45px); 13 | background-color: #f8ffaf; 14 | } 15 | } 16 | } 17 | 18 | label { 19 | display: flex; 20 | width: 90px; 21 | height: 45px; 22 | border: 3px solid #111; 23 | border-radius: 99em; 24 | position: relative; 25 | transition: transform .5s ease-in-out; 26 | transform-origin: 50% 50%; 27 | cursor: pointer; 28 | 29 | &:before { 30 | transition: transform 1s ease; 31 | transition-delay: .5s; 32 | content: ""; 33 | display: block; 34 | position: absolute; 35 | width: 27px; 36 | height: 27px; 37 | background-color: #111; 38 | border-radius: 50%; 39 | top: 6px; 40 | left: 6px; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-list", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.2", 7 | "@testing-library/react": "^12.1.3", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^17.0.2", 10 | "react-beautiful-dnd": "^13.1.0", 11 | "react-dom": "^17.0.2", 12 | "react-icons": "^4.3.1", 13 | "react-scripts": "5.0.0", 14 | "sass": "^1.49.9", 15 | "uuidv4": "^6.2.12", 16 | "web-vitals": "^2.1.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "CI= react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ilyas Belaoud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/List.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Droppable } from "react-beautiful-dnd"; 3 | import { useGlobalContext } from "./context"; 4 | import Task from "./Task"; 5 | 6 | const List = () => { 7 | const { tasks, filter } = useGlobalContext(); 8 | 9 | let filtred = [...tasks]; 10 | 11 | switch (filter) { 12 | case "all": 13 | filtred = [...tasks]; 14 | break; 15 | case "completed": 16 | filtred = tasks.filter((task) => task.completed); 17 | break; 18 | case "uncompleted": 19 | filtred = tasks.filter((task) => !task.completed); 20 | break; 21 | default: 22 | filtred = [...tasks]; 23 | break; 24 | } 25 | 26 | return ( 27 | 28 | {(provided, snapshot) => ( 29 | 39 | )} 40 | 41 | ); 42 | }; 43 | 44 | export default List; 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Todo App 2 | 3 | A minimalistic, customisable todo app. 4 | 5 | ## Demo 6 | 7 | - Demo hosted on Netlify: [https://react-todo-list-9.netlify.app/](https://react-todo-list-9.netlify.app/) 8 | 9 | ## Features 10 | 11 | - Dark mode 12 | - Drag n drop tasks to reorder them 13 | - Tasks saved locally 14 | - Customisable task color 15 | - Edit task 16 | - Notification Box 17 | - Tablet & mobile friendly 18 | 19 | ## Built with 20 | 21 | - React Js, Css, Sass 22 | - React Hooks : useState, useEffect, useRef, useContext 23 | - react-beautiful-dnd, react-icons, uuidv4... 24 | 25 | ## License 26 | 27 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for more details 28 | 29 | ## Author 30 | 31 | - Instagram - [@ilyasbelaoud](https://www.instagram.com/ilyasbelaoud) 32 | - Twitter - [@ilyasbelaoud](https://www.twitter.com/ilyasbelaoud) 33 | 34 | ## Instructions 35 | 36 | First clone this repository. 37 | ```bash 38 | $ git clone https://github.com/ilyasbelaoud/react-todo-app.git 39 | ``` 40 | 41 | Install dependencies. Make sure you already have [`nodejs`](https://nodejs.org/en/) & [`npm`](https://www.npmjs.com/) installed in your system. 42 | ```bash 43 | $ npm install # or yarn 44 | ``` 45 | 46 | Run it 47 | ```bash 48 | $ npm start # or yarn start 49 | ``` -------------------------------------------------------------------------------- /src/Colors.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | import { useGlobalContext } from "./context"; 3 | 4 | const Colors = () => { 5 | const { location, setIsColorsOpen, tasks, setTasks } = useGlobalContext(); 6 | const colorsRef = useRef(null); 7 | 8 | useEffect(() => { 9 | const { top, right } = location; 10 | colorsRef.current.style.left = `${right + 30}px`; 11 | colorsRef.current.style.top = `${top - 20}px`; 12 | }, [location]); 13 | 14 | const changeColor = (e) => { 15 | const color = e.target.style.backgroundColor; 16 | const { id } = location; 17 | setTasks( 18 | tasks.map((task) => { 19 | return task.id === id ? { ...task, color: color } : task; 20 | }) 21 | ); 22 | setIsColorsOpen(false); 23 | }; 24 | 25 | return ( 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | ); 38 | }; 39 | 40 | export default Colors; 41 | -------------------------------------------------------------------------------- /src/Task.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Draggable } from "react-beautiful-dnd"; 3 | import { 4 | MdCheckBoxOutlineBlank, 5 | MdCheckBox, 6 | MdDeleteOutline, 7 | MdOutlineColorLens, 8 | } from "react-icons/md"; 9 | import { FiEdit } from "react-icons/fi"; 10 | import { useGlobalContext } from "./context"; 11 | 12 | const Task = ({ id, name, completed, color, index }) => { 13 | const { removeTask, toggleDone, editTask, showColors } = useGlobalContext(); 14 | 15 | return ( 16 | 17 | {(provided, snapshot) => ( 18 |
  • 32 |

    {name}

    33 | 36 | 39 | 42 | 45 |
  • 46 | )} 47 |
    48 | ); 49 | }; 50 | 51 | export default Task; 52 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, useRef } from "react"; 2 | 3 | const getTasks = () => { 4 | const tasks = localStorage.getItem("tasks"); 5 | return tasks ? JSON.parse(tasks) : []; 6 | }; 7 | 8 | const AppContext = React.createContext(null); 9 | 10 | const AppProvider = ({ children }) => { 11 | const inputRef = useRef(null); 12 | const [tasks, setTasks] = useState(getTasks()); 13 | const [alert, setAlert] = useState({ show: false, msg: "" }); 14 | const [isEditing, setIsEditing] = useState(false); 15 | const [editId, setEditId] = useState(null); 16 | const [name, setName] = useState(""); 17 | const [filter, setFilter] = useState("all"); 18 | const [isColorsOpen, setIsColorsOpen] = useState(false); 19 | const [location, setLocation] = useState({}); 20 | const refContainer = useRef(null); 21 | 22 | const removeTask = (id) => { 23 | setTasks(tasks.filter((task) => task.id !== id)); 24 | showAlert(true, "Task Removed."); 25 | }; 26 | 27 | const toggleDone = (id) => { 28 | setTasks( 29 | tasks.map((task) => 30 | task.id === id ? { ...task, completed: !task.completed } : task 31 | ) 32 | ); 33 | showAlert(true, "Task State Changed."); 34 | }; 35 | 36 | const editTask = (id) => { 37 | const { name } = tasks.find((task) => task.id === id); 38 | setIsEditing(true); 39 | setEditId(id); 40 | setName(name); 41 | inputRef.current.focus(); 42 | }; 43 | 44 | const showAlert = (show, msg) => { 45 | setAlert({ show, msg }); 46 | }; 47 | 48 | const showColors = (e, id) => { 49 | const { top, right } = e.target.getBoundingClientRect(); 50 | setLocation({ top, right, id }); 51 | setIsColorsOpen(true); 52 | }; 53 | 54 | return ( 55 | 82 | {children} 83 | 84 | ); 85 | }; 86 | 87 | const useGlobalContext = () => { 88 | return useContext(AppContext); 89 | }; 90 | 91 | export { AppContext, AppProvider, useGlobalContext }; 92 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import {FaGithub} from 'react-icons/fa' 3 | import { v4 as uuid } from "uuid"; 4 | import { DragDropContext } from "react-beautiful-dnd"; 5 | import List from "./List"; 6 | import Alert from "./Alert"; 7 | import { useGlobalContext } from "./context"; 8 | import Colors from "./Colors"; 9 | import DarkModeToggle from './DarkModeToggle'; 10 | 11 | const App = () => { 12 | const { 13 | inputRef, 14 | tasks, 15 | setTasks, 16 | alert, 17 | showAlert, 18 | isEditing, 19 | setIsEditing, 20 | editId, 21 | setEditId, 22 | name, 23 | setName, 24 | filter, 25 | setFilter, 26 | isColorsOpen, 27 | setIsColorsOpen, 28 | } = useGlobalContext(); 29 | 30 | const addTask = (e) => { 31 | e.preventDefault(); 32 | if (!name) { 33 | showAlert(true, "Invalid Task Name!"); 34 | } else if (name && isEditing) { 35 | setTasks( 36 | tasks.map((task) => { 37 | return task.id === editId ? { ...task, name: name } : task; 38 | }) 39 | ); 40 | setIsEditing(false); 41 | setEditId(null); 42 | setName(""); 43 | showAlert(true, "Task Edited."); 44 | } else { 45 | const newTask = { 46 | id: uuid().slice(0, 8), 47 | name: name, 48 | completed: false, 49 | color: "#009688", 50 | }; 51 | setTasks([...tasks, newTask]); 52 | showAlert(true, "Task Added."); 53 | setName(""); 54 | } 55 | }; 56 | 57 | const filterTasks = (e) => { 58 | setFilter(e.target.dataset["filter"]); 59 | }; 60 | 61 | const deleteAll = () => { 62 | setTasks([]); 63 | showAlert(true, "Your list is clear!"); 64 | }; 65 | 66 | useEffect(() => { 67 | inputRef.current.focus(); 68 | localStorage.setItem("tasks", JSON.stringify(tasks)); 69 | }, [inputRef, tasks]); 70 | 71 | const handleDragEnd = (param) => { 72 | const srcI = param.source.index; 73 | const desI = param.destination?.index; 74 | if (desI) { 75 | const reOrdered = [...tasks]; 76 | reOrdered.splice(desI, 0, reOrdered.splice(srcI, 1)[0]); 77 | setTasks(reOrdered); 78 | } 79 | }; 80 | 81 | const hideColorsContainer = (e) => { 82 | // body. 83 | if (e.target.classList.contains("btn-colors")) return; 84 | setIsColorsOpen(false); 85 | }; 86 | 87 | return ( 88 | <> 89 |
    90 | {isColorsOpen && } 91 | {alert && } 92 |
    93 | setName(e.target.value)} 99 | /> 100 | 101 |
    102 |
    103 | 110 | 117 | 124 |
    125 | 126 | {tasks.length > 0 ? ( 127 | 128 | ) : ( 129 |

    Your list is clear!

    130 | )} 131 |
    132 | {tasks.length > 2 && ( 133 | 140 | )} 141 | 142 | 143 |
    144 | 147 | 148 | ); 149 | }; 150 | 151 | export default App; 152 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Nunito:wght@200;400;500&display=swap"); 2 | 3 | :root { 4 | --bg-color: #94d8e3; 5 | --alt-bg-color: #0000004d; 6 | --font-color: #ffffff; 7 | --alt-font-color: #222222; 8 | --primary-color: #607d8b; 9 | --secondary-color: #93B5C6; 10 | } 11 | .dark { 12 | --bg-color: #003950; 13 | --alt-bg-color: #0000004d; 14 | --font-color: #ffffff; 15 | --alt-font-color: #222222; 16 | --primary-color: #008e9f; 17 | --secondary-color: #00e4ff; 18 | } 19 | * { 20 | margin: 0; 21 | padding: 0; 22 | box-sizing: border-box; 23 | } 24 | body { 25 | min-height: 100vh; 26 | background: var(--bg-color); 27 | color: var(--font-color); 28 | font-family: "Nunito", sans-serif; 29 | padding: 80px 0; 30 | position: relative; 31 | font-size: 100%; 32 | transition: all 0.3s linear; 33 | } 34 | .container { 35 | width: 80%; 36 | max-width: 900px; 37 | padding: 30px; 38 | margin: 0 auto; 39 | background: var(--alt-bg-color); 40 | border-radius: 20px; 41 | text-align: center; 42 | box-shadow: 0 0 20px 0px #00000045; 43 | } 44 | @media (max-width: 768px) { 45 | .container { 46 | padding: 20px; 47 | width: 90%; 48 | } 49 | } 50 | .container .head { 51 | width: 100%; 52 | display: grid; 53 | grid-template-columns: 70% 29%; 54 | gap: 1%; 55 | margin-bottom: 50px; 56 | } 57 | @media (max-width: 768px) { 58 | .container .head { 59 | grid-template-columns: 1fr; 60 | gap: 5px; 61 | } 62 | } 63 | .head input { 64 | padding: 13px 20px; 65 | outline: none; 66 | border: none; 67 | background-color: #eee; 68 | border-radius: 10px; 69 | font-size: 0.9rem; 70 | font-family: inherit; 71 | } 72 | button { 73 | cursor: pointer; 74 | background-color: var(--primary-color); 75 | padding: 10px 30px; 76 | border-radius: 10px; 77 | border: none; 78 | font-size: 0.9rem; 79 | font-weight: 500; 80 | font-family: inherit; 81 | color: var(--font-color); 82 | transition: all 0.3s ease; 83 | } 84 | button:hover { 85 | opacity: 0.7; 86 | } 87 | .filter { 88 | margin: 0 auto 40px; 89 | display: flex; 90 | justify-content: center; 91 | align-items: center; 92 | gap: 5px; 93 | } 94 | @media (max-width: 576px) { 95 | .filter { 96 | flex-direction: column; 97 | } 98 | } 99 | .filter button.active { 100 | background-color: var(--secondary-color); 101 | color: #333; 102 | } 103 | .tasks-wrapper { 104 | list-style: none; 105 | padding: 0 50px; 106 | display: flex; 107 | flex-direction: column; 108 | gap: 20px; 109 | color: var(--font-color); 110 | max-height: 400px; 111 | overflow-y: auto; 112 | } 113 | @media (max-width: 768px) { 114 | .tasks-wrapper { 115 | padding: 0 5px; 116 | gap: 5px; 117 | } 118 | } 119 | .task { 120 | background-color: var(--primary-color); 121 | padding: 4px 20px 5px 30px; 122 | border-radius: 10px; 123 | display: flex; 124 | gap: 5px; 125 | align-items: center; 126 | text-align: left; 127 | transition: all 0.5s ease; 128 | } 129 | .task p { 130 | font-size: 1rem; 131 | text-transform: capitalize; 132 | flex: 1; 133 | } 134 | .task button { 135 | background-color: transparent; 136 | color: var(--alt-font-color); 137 | width: 40px; 138 | height: 40px; 139 | border-radius: 50%; 140 | padding: 0; 141 | font-weight: bold; 142 | font-size: 1rem; 143 | display: flex; 144 | align-items: center; 145 | justify-content: center; 146 | transition: 0.7s; 147 | } 148 | .task button:hover { 149 | color: var(--primary-color); 150 | background-color: var(--alt-font-color); 151 | opacity: 0.85; 152 | border-radius: 10px; 153 | } 154 | .tasks-wrapper .task-done { 155 | opacity: 0.5; 156 | } 157 | .tasks-wrapper .task-done p { 158 | text-decoration: line-through; 159 | font-style: italic; 160 | } 161 | button.btn-delete-all { 162 | margin-top: 50px; 163 | background-color: #ba0f30; 164 | } 165 | .no-tasks { 166 | color: var(--font-color); 167 | } 168 | .alert { 169 | margin-bottom: 1rem; 170 | width: 230px; 171 | height: 80px; 172 | display: grid; 173 | align-items: center; 174 | text-align: center; 175 | font-size: 0.9rem; 176 | color: var(--alt-font-color); 177 | border-radius: 0.25rem; 178 | letter-spacing: var(--spacing); 179 | text-transform: capitalize; 180 | background: #eee; 181 | position: absolute; 182 | bottom: 0; 183 | left: -100%; 184 | transition: all 0.5s cubic-bezier(0.6, -0.28, 0.74, 0.05); 185 | box-shadow: 0 0 20px 0px #00000045; 186 | } 187 | .color-container { 188 | position: absolute; 189 | width: 100px; 190 | height: 80px; 191 | display: grid; 192 | grid-template-columns: repeat(3, 1fr); 193 | background: #eee; 194 | padding: 3px; 195 | gap: 3px; 196 | border-radius: 10px; 197 | z-index: 3; 198 | } 199 | .color-container > span { 200 | border-radius: 5px; 201 | } 202 | ::-webkit-scrollbar { 203 | width: 0.5rem; 204 | border-radius: 10px; 205 | } 206 | ::-webkit-scrollbar-track { 207 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 208 | border-radius: 10px; 209 | } 210 | ::-webkit-scrollbar-thumb { 211 | background-color: #00b3b3; 212 | outline: 0px inset #7fffd4; 213 | border-radius: 10px; 214 | height: 50px; 215 | } 216 | .preventClick { 217 | pointer-events: none; 218 | } 219 | .footer { 220 | position: absolute; 221 | bottom: 0; 222 | width: 100%; 223 | text-align: center; 224 | margin: 10px auto; 225 | } 226 | .github { 227 | font-size: 2rem; 228 | color: darkcyan; 229 | } --------------------------------------------------------------------------------