diff --git a/package-lock.json b/package-lock.json index a58d3d17..02ef0408 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2140,6 +2140,11 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "@types/lodash": { + "version": "4.14.170", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz", + "integrity": "sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q==" + }, "@types/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", @@ -3951,6 +3956,11 @@ } } }, + "classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -6937,6 +6947,27 @@ "mime-types": "^2.1.12" } }, + "formik": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.2.7.tgz", + "integrity": "sha512-j4cso6QL90T8hJWgU29GYsBQuj1vynBfrcURyK91KshArvS5CLoxUkP52hKc3wVpGFACd0uWEJo7y30ZOTzc5g==", + "requires": { + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.14", + "lodash-es": "^4.17.14", + "react-fast-compare": "^2.0.1", + "tiny-warning": "^1.0.2", + "tslib": "^1.10.0" + }, + "dependencies": { + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" + } + } + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -9433,6 +9464,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", @@ -9914,6 +9950,11 @@ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", "optional": true }, + "nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==" + }, "nanoid": { "version": "3.1.23", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", @@ -11938,6 +11979,11 @@ } } }, + "property-expr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.4.tgz", + "integrity": "sha512-sFPkHQjVKheDNnPvotjQmm3KD3uk1fWKUN7CrpdbwmUx3CrG3QiM8QpTSimvig5vTXmTvjz7+TDvXOI9+4rkcg==" + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -12333,6 +12379,16 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==" }, + "react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, + "react-icons": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.2.0.tgz", + "integrity": "sha512-rmzEDFt+AVXRzD7zDE21gcxyBizD/3NqjbX6cmViAgdqfJ2UiLer8927/QhhrXQV7dEj/1EGuOTPp7JnLYVJKQ==" + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -14793,6 +14849,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" + }, "tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -15267,8 +15328,7 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "optional": true + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { "version": "2.3.0", @@ -16880,6 +16940,20 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "yup": { + "version": "0.32.9", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.9.tgz", + "integrity": "sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==", + "requires": { + "@babel/runtime": "^7.10.5", + "@types/lodash": "^4.14.165", + "lodash": "^4.17.20", + "lodash-es": "^4.17.15", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + } } } } diff --git a/package.json b/package.json index 326aeb06..ca03b706 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,21 @@ "@testing-library/react": "^11.2.6", "@testing-library/user-event": "^13.1.5", "bootstrap": "^4.6.0", + "classnames": "^2.3.1", + "formik": "^2.2.7", "prop-types": "^15.7.2", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-icons": "^4.2.0", "react-redux": "^7.2.3", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "redux": "^4.0.5", "redux-thunk": "^2.3.0", "sass": "^1.32.11", - "web-vitals": "^1.1.1" + "uuid": "^8.3.2", + "web-vitals": "^1.1.1", + "yup": "^0.32.9" }, "devDependencies": { "@babel/core": "^7.13.16", diff --git a/src/App.js b/src/App.js index de524524..88704047 100644 --- a/src/App.js +++ b/src/App.js @@ -1,15 +1,215 @@ -import React from "react"; - -function App() { - return ( -
-
-
-

Hola mundo

-
-
-
- ); +import React, { Component } from "react"; +import classNames from "classnames"; +import { Route } from "react-router-dom"; +import { v4 as uuidv4 } from "uuid"; + +import TodoList from "./components/TodoList"; +import * as api from "./api"; +import AppHeader from "./components/AppHeader"; + +import { HOME, ACTIVE, COMPLETED } from "./constatnts/routes"; + +import "./app.scss"; + +const LOCAL_STORAGE_KEY = "todo-state"; + +function loadLocalStorageData() { + const prevItems = localStorage.getItem(LOCAL_STORAGE_KEY); + + if (!prevItems) { + return null; + } + + try { + return JSON.parse(prevItems); + } catch (error) { + return null; + } +} + +class App extends Component { + constructor(props) { + super(props); + this.state = { + todos: [], + todoName: "", + currentTheme: false, + }; + this.handleAddTodo = this.handleAddTodo.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleRemove = this.handleRemove.bind(this); + this.handleChangeCheck = this.handleChangeCheck.bind(this); + this.handleEdit = this.handleEdit.bind(this); + this.handleEditSubmit = this.handleEditSubmit.bind(this); + this.handleResetEdit = this.handleResetEdit.bind(this); + this.handleClearCompleted = this.handleClearCompleted.bind(this); + this.handleThemeClick = this.handleThemeClick.bind(this); + } + + componentDidMount() { + const prevItems = loadLocalStorageData(); + + if (!prevItems || !prevItems.todos.length) { + api.getProducts().then((data) => { + this.setState({ todos: data }); + }); + return; + } + + this.setState({ + todos: prevItems.todos, + currentTheme: prevItems.currentTheme, + }); + } + + componentDidUpdate() { + const { todos, currentTheme } = this.state; + localStorage.setItem( + LOCAL_STORAGE_KEY, + JSON.stringify({ todos, currentTheme }), + ); + } + + handleAddTodo(values) { + const { todos } = this.state; + + const newTodo = { + id: uuidv4(), + name: values.name, + complete: false, + }; + + this.setState({ todos: [...todos, newTodo], todoName: "" }); + } + + handleSubmit(e) { + e.preventDefault(); + this.handleAddTodo(this.state); + } + + handleChange(e) { + this.setState({ todoName: e.target.value }); + } + + handleRemove(id) { + const { todos } = this.state; + const arr = todos.filter((todo) => todo.id !== id); + this.setState({ todos: arr }); + } + + handleChangeCheck(id) { + const { todos } = this.state; + const arr = todos.map((todo) => { + return todo.id === id ? { ...todo, complete: !todo.complete } : todo; + // return obj; + }); + + this.setState({ todos: arr }); + } + + handleClearCompleted() { + const { todos } = this.state; + const arr = todos.filter((todo) => todo.complete === false); + this.setState({ todos: arr }); + } + + handleEdit(id) { + const { todos } = this.state; + const todoToEdit = todos.map((todo) => { + return todo.id === id ? { ...todo, edit: true } : todo; + }); + this.setState({ todos: todoToEdit }); + } + + handleEditSubmit(values, id) { + const { todos } = this.state; + const todoToEdit = todos.map((todo) => { + return todo.id === id + ? { ...todo, name: values.name, edit: false } + : todo; + }); + this.setState({ todos: todoToEdit }); + } + + handleResetEdit() { + const { todos } = this.state; + const todoToEdit = todos.map((todo) => { + return { ...todo, edit: false }; + }); + this.setState({ todos: todoToEdit }); + } + + handleThemeClick() { + const { currentTheme } = this.state; + this.setState({ currentTheme: !currentTheme }); + } + + render() { + const { todos, currentTheme } = this.state; + const appClasses = classNames({ + globalContainer: true, + darkModeOpacity: currentTheme, + }); + + return ( +
+ + + ( + !todo.complete)} + handleClearCompleted={this.handleClearCompleted} + currentTheme={currentTheme} + /> + )} + /> + + ( + todo.complete)} + handleClearCompleted={this.handleClearCompleted} + currentTheme={currentTheme} + /> + )} + /> + + ( + + )} + /> +
+ ); + } } export default App; diff --git a/src/api/getProducts.js b/src/api/getProducts.js new file mode 100644 index 00000000..17377d03 --- /dev/null +++ b/src/api/getProducts.js @@ -0,0 +1,15 @@ +import todos from "../utils/demo-data"; + +function getProducts(fail = false) { + return new Promise((res, rej) => { + setTimeout(() => { + if (fail) { + rej(new Error("Failed to fetch")); + } + + res(todos); + }, 100); + }); +} + +export { getProducts }; diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 00000000..3e032830 --- /dev/null +++ b/src/api/index.js @@ -0,0 +1 @@ +export * from "./getProducts"; diff --git a/src/app.scss b/src/app.scss new file mode 100644 index 00000000..e6686296 --- /dev/null +++ b/src/app.scss @@ -0,0 +1,33 @@ +//light theme +$bg-theme-light: #fafafa; +$color-theme-light: #222; + +//dark theme_color +$bg-theme-dark: #222; +$color-theme-dark: #fff; + +body { + background-color: $bg-theme-light; + color: $color-theme-light; + height: 100vh; +} + +* { + padding: 0; + margin: 0; + box-sizing: border-box; + // outline: 1px solid black; +} + +.globalContainer { + min-height: 100vh; +} + +.darkMode { + background-color: rgba($color: $bg-theme-dark, $alpha: 1); + color: $color-theme-dark; +} +.darkModeOpacity { + background-color: rgba($color: $bg-theme-dark, $alpha: 0.6); + color: $color-theme-dark; +} diff --git a/src/components/AppFooter/AppFooter.js b/src/components/AppFooter/AppFooter.js new file mode 100644 index 00000000..5edbeaa5 --- /dev/null +++ b/src/components/AppFooter/AppFooter.js @@ -0,0 +1,57 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; + +import "./AppFooter.scss"; +import classNames from "classnames"; + +import { HOME, ACTIVE, COMPLETED } from "../../constatnts/routes"; + +function AppFooter({ todos, handleClearCompleted, currentTheme }) { + function onHandleClearCompleted() { + handleClearCompleted(); + } + const footerClearCompleteClasses = classNames({ + main__footer__link: true, + main__footer__linkDarkMode: currentTheme, + }); + return ( +
+ {todos.filter((v) => !v.complete).length} items left +
+ + All + + + Active + + + Completed + +
+ +
+ ); +} + +export default AppFooter; diff --git a/src/components/AppFooter/AppFooter.scss b/src/components/AppFooter/AppFooter.scss new file mode 100644 index 00000000..a891e9b8 --- /dev/null +++ b/src/components/AppFooter/AppFooter.scss @@ -0,0 +1,25 @@ +.main__footer { + &__link { + text-decoration: none; + color: black; + opacity: 0.7; + } + + &__link:hover { + text-decoration: none; + color: rgba($color: #6c63ff, $alpha: 1); + } +} + +.selected { + color: rgba($color: #6c63ff, $alpha: 1); + font-weight: bold; +} + +@media only screen and (max-width: 500px) { + .main__footer { + display: flex; + flex-direction: column; + align-items: center; + } +} diff --git a/src/components/AppFooter/index.js b/src/components/AppFooter/index.js new file mode 100644 index 00000000..569644b5 --- /dev/null +++ b/src/components/AppFooter/index.js @@ -0,0 +1 @@ +export { default } from "./AppFooter"; diff --git a/src/components/AppHeader/AppHeader.js b/src/components/AppHeader/AppHeader.js new file mode 100644 index 00000000..030dfaef --- /dev/null +++ b/src/components/AppHeader/AppHeader.js @@ -0,0 +1,81 @@ +import React from "react"; +import { Formik } from "formik"; +import classNames from "classnames"; + +import Checkbox from "../Checkbox"; +import Input from "../Input"; +import ThemeToggle from "../ThemeToggle"; + +import productSchema from "./todo-schema"; + +import hero from "../../img/hero.jpg"; +import "./AppHeader.scss"; + +function Appheader({ handleAddTodo, handleThemeClick, currentTheme }) { + const headerClasses = classNames({ + TODO__Header: true, + TODO__Header__DarkMode: currentTheme, + }); + + const formClasses = classNames({ + TODO__Form: true, + TODO__Form__DarkMode: currentTheme, + }); + + const heroClasses = classNames({ + heroImg: true, + heroImg__darkMode: currentTheme, + }); + + return ( +
+
+
+

TODO

+ +
+ { + handleAddTodo(values); + resetForm(); + }} + > + {({ + handleChange, + handleBlur, + handleSubmit, + errors, + values, + touched, + }) => ( +
+ {}} /> + + + )} +
+
+
+ ); +} + +export default Appheader; diff --git a/src/components/AppHeader/AppHeader.scss b/src/components/AppHeader/AppHeader.scss new file mode 100644 index 00000000..9e5b321f --- /dev/null +++ b/src/components/AppHeader/AppHeader.scss @@ -0,0 +1,56 @@ +.heroImg { + min-height: 210px; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: linear-gradient( + 135deg, + hsl(348, 97%, 63%, 0.65), + hsl(230, 96%, 62%, 0.65) + ), + url("../../img/hero.jpg"); + background-position: center; +} + +.heroImg__darkMode { + background: linear-gradient( + 135deg, + rgba(241, 66, 101, 0.65), + rgba(7, 11, 27, 0.65) + ), + url("../../img/hero.jpg"); + background-position: center; +} + +.TODO__Header { + margin: 0px; + width: 100%; + max-width: 500px; + flex-grow: 1; + display: inline; + text-align: left; + letter-spacing: 10px; + color: white; +} +.TODO__Header__DarkMode { + color: #222; +} +.TODO__Form { + display: flex; + justify-content: flex-start; + align-items: center; + background-color: white; + margin-bottom: 3rem; + border-radius: 5px; + overflow: hidden; + padding-left: 0.7rem; + width: 100%; + max-width: 500px; +} + +.TODO__Form__DarkMode { + background-color: #222; + color: white; +} diff --git a/src/components/AppHeader/index.js b/src/components/AppHeader/index.js new file mode 100644 index 00000000..8ad0d174 --- /dev/null +++ b/src/components/AppHeader/index.js @@ -0,0 +1 @@ +export { default } from "./AppHeader"; diff --git a/src/components/AppHeader/todo-schema.js b/src/components/AppHeader/todo-schema.js new file mode 100644 index 00000000..7e25179f --- /dev/null +++ b/src/components/AppHeader/todo-schema.js @@ -0,0 +1,10 @@ +import * as Yup from "yup"; + +const productSchema = Yup.object().shape({ + name: Yup.string() + .required("The todo name is required") + .min(2, "The todo name is too short!") + .max(50, "The todo name is too long!"), +}); + +export default productSchema; diff --git a/src/components/Checkbox/Checkbox.js b/src/components/Checkbox/Checkbox.js new file mode 100644 index 00000000..51536f5a --- /dev/null +++ b/src/components/Checkbox/Checkbox.js @@ -0,0 +1,12 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import React from "react"; +import "./Checkbox.scss"; + +export default function Checkbox({ checked, handleChangeCheck, id }) { + return ( +
handleChangeCheck(id)}> +
+
+ ); +} diff --git a/src/components/Checkbox/Checkbox.scss b/src/components/Checkbox/Checkbox.scss new file mode 100644 index 00000000..4ea6bed4 --- /dev/null +++ b/src/components/Checkbox/Checkbox.scss @@ -0,0 +1,41 @@ +.border { + position: relative; + display: flex; + justify-content: center; + align-items: center; + margin: 0 8px; + width: 20px; + height: 20px; + border: 2px solid grey; + border-radius: 25px; +} + +.indicator { + position: relative; + width: 15px; + height: 15px; + border-radius: 25px; + z-index: 1; + background: linear-gradient( + 135deg, + hsl(348, 97%, 63%, 1), + hsl(230, 96%, 62%, 1) + ); + transform: scale(0); + transition: transform 100ms; +} + +.indicator::after { + position: absolute; + top: 40%; + left: 50%; + width: 5px; + height: 9px; + content: ""; + border-right: 2px solid white; + border-bottom: 2px solid white; + transform: translate(-50%, -50%) rotate(40deg); +} +.checked { + transform: scale(1.1); +} diff --git a/src/components/Checkbox/index.js b/src/components/Checkbox/index.js new file mode 100644 index 00000000..a936c852 --- /dev/null +++ b/src/components/Checkbox/index.js @@ -0,0 +1 @@ +export { default } from "./Checkbox"; diff --git a/src/components/EditTodo/EditTodo.js b/src/components/EditTodo/EditTodo.js new file mode 100644 index 00000000..11a20807 --- /dev/null +++ b/src/components/EditTodo/EditTodo.js @@ -0,0 +1,51 @@ +/* eslint-disable react/jsx-boolean-value */ +import React from "react"; +import { Formik } from "formik"; + +import Input from "../Input"; +import editSchema from "./edit-schema"; + +import "./EditTodo.scss"; + +function EditTodo({ handleEditSubmit, handleResetEdit, todo, currentTheme }) { + return ( + { + handleEditSubmit(values, todo.id); + }} + > + {({ + handleChange, + handleBlur, + handleSubmit, + errors, + values, + touched, + }) => ( +
+ { + handleBlur(e); + handleResetEdit(); + }} + hasErrorMessage={touched.name} + errorMessage={errors.name} + autoFocus={true} + currentTheme={currentTheme} + /> +
+ )} +
+ ); +} + +export default EditTodo; diff --git a/src/components/EditTodo/EditTodo.scss b/src/components/EditTodo/EditTodo.scss new file mode 100644 index 00000000..9a07f62b --- /dev/null +++ b/src/components/EditTodo/EditTodo.scss @@ -0,0 +1,5 @@ +.Edit__Form { + display: flex; + align-items: center; + flex-grow: 1; +} diff --git a/src/components/EditTodo/edit-schema.js b/src/components/EditTodo/edit-schema.js new file mode 100644 index 00000000..e92813fc --- /dev/null +++ b/src/components/EditTodo/edit-schema.js @@ -0,0 +1,10 @@ +import * as Yup from "yup"; + +const editSchema = Yup.object().shape({ + name: Yup.string() + .required("The todo name is required") + .min(2, "The todo name is too short!") + .max(50, "The todo name is too long!"), +}); + +export default editSchema; diff --git a/src/components/EditTodo/index.js b/src/components/EditTodo/index.js new file mode 100644 index 00000000..17c94385 --- /dev/null +++ b/src/components/EditTodo/index.js @@ -0,0 +1 @@ +export { default } from "./EditTodo"; diff --git a/src/components/EmptyTodo/EmptyTodo.js b/src/components/EmptyTodo/EmptyTodo.js new file mode 100644 index 00000000..065ab95a --- /dev/null +++ b/src/components/EmptyTodo/EmptyTodo.js @@ -0,0 +1,15 @@ +import React from "react"; + +import illustration from "../../img/illustration.svg"; +import "./EmptyTodo.scss"; + +function EmptyTodo() { + return ( +
+

There is no todo...

+ illustration +
+ ); +} + +export default EmptyTodo; diff --git a/src/components/EmptyTodo/EmptyTodo.scss b/src/components/EmptyTodo/EmptyTodo.scss new file mode 100644 index 00000000..3c6e09c5 --- /dev/null +++ b/src/components/EmptyTodo/EmptyTodo.scss @@ -0,0 +1,7 @@ +.main__empty { + p { + color: rgba($color: #6c63ff, $alpha: 1); + font-size: 1.3rem; + font-weight: bold; + } +} diff --git a/src/components/EmptyTodo/index.js b/src/components/EmptyTodo/index.js new file mode 100644 index 00000000..97fec86c --- /dev/null +++ b/src/components/EmptyTodo/index.js @@ -0,0 +1 @@ +export { default } from "./EmptyTodo"; diff --git a/src/components/Input/Input.js b/src/components/Input/Input.js new file mode 100644 index 00000000..f30b942f --- /dev/null +++ b/src/components/Input/Input.js @@ -0,0 +1,48 @@ +/* eslint-disable jsx-a11y/no-autofocus */ +import classNames from "classnames"; +import React from "react"; + +import "./Input.scss"; + +function Input({ + type = "text", + id = "input-01", + value = "", + placeholder = "", + handleChange = () => {}, + handleBlur = () => {}, + errorMessage, + hasErrorMessage, + autoFocus, + currentTheme, + ...props +}) { + const classes = classNames({ + input__class: true, + "input-class-err": hasErrorMessage && errorMessage, + "is-invalid": hasErrorMessage && errorMessage, + input__class__darkMode: currentTheme, + }); + + return ( + <> + + {hasErrorMessage && errorMessage && ( +

{errorMessage}

+ )} + + ); +} + +export default Input; diff --git a/src/components/Input/Input.scss b/src/components/Input/Input.scss new file mode 100644 index 00000000..6d656e57 --- /dev/null +++ b/src/components/Input/Input.scss @@ -0,0 +1,21 @@ +.invalid-msg { + color: red; + font-size: 80%; + margin: 0; + padding: 0; +} + +.input__class { + flex-grow: 1; + padding: 0.7rem; + border: none; + outline: none; + background: transparent; +} + +.input__class__darkMode { + color: white; +} +.input-class-err { + flex-grow: 0; +} diff --git a/src/components/Input/index.js b/src/components/Input/index.js new file mode 100644 index 00000000..a50d7d11 --- /dev/null +++ b/src/components/Input/index.js @@ -0,0 +1 @@ +export { default } from "./Input"; diff --git a/src/components/ThemeToggle/ThemeToggle.js b/src/components/ThemeToggle/ThemeToggle.js new file mode 100644 index 00000000..2e736264 --- /dev/null +++ b/src/components/ThemeToggle/ThemeToggle.js @@ -0,0 +1,36 @@ +import React from "react"; +import classNames from "classnames"; + +import "./ThemeToggle.scss"; + +function ThemeToggle({ handleThemeClick, currentTheme }) { + function onHandleThemeClick() { + handleThemeClick(); + } + const themeSwitcherClasses = classNames({ + themeSwitcherButton: true, + themeSwitcherButton__active: currentTheme, + }); + + return ( +
+ + ☀️ + + + + 🌙 + +
+ ); +} + +export default ThemeToggle; diff --git a/src/components/ThemeToggle/ThemeToggle.scss b/src/components/ThemeToggle/ThemeToggle.scss new file mode 100644 index 00000000..b186ee53 --- /dev/null +++ b/src/components/ThemeToggle/ThemeToggle.scss @@ -0,0 +1,47 @@ +.themeToggle { + display: flex; +} + +.themeSwitcherWrap { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + max-width: 500px; + margin-bottom: 20px; + + .themeSwitcherButton { + margin: 0px 3px; + margin-top: 3px; + display: inline-block; + max-height: 20px; + cursor: pointer; + background: transparent; + border: none; + + .switchPath { + width: 40px; + height: 20px; + border-radius: 10px; + background-color: #ccc; + transition: all 0.3s ease-in-out; + + .switchHandle { + background-color: #999; + width: 20px; + height: 20px; + border-radius: 50%; + transition: all 0.3s ease-in-out; + } + } + + &__active { + .switchPath { + background-color: #555; + } + .switchHandle { + transform: translateX(20px); + } + } + } +} diff --git a/src/components/ThemeToggle/index.js b/src/components/ThemeToggle/index.js new file mode 100644 index 00000000..b3c9211c --- /dev/null +++ b/src/components/ThemeToggle/index.js @@ -0,0 +1 @@ +export { default } from "./ThemeToggle"; diff --git a/src/components/Todo/Todo.js b/src/components/Todo/Todo.js new file mode 100644 index 00000000..b57210c9 --- /dev/null +++ b/src/components/Todo/Todo.js @@ -0,0 +1,71 @@ +import classNames from "classnames"; +import React from "react"; +import { AiOutlineClose } from "react-icons/ai"; + +import Checkbox from "../Checkbox"; +import EditTodo from "../EditTodo"; + +import "./Todo.scss"; + +function Todo({ + todo = {}, + handleRemove = () => {}, + handleChangeCheck = () => {}, + handleEdit = () => {}, + handleEditSubmit = () => {}, + handleResetEdit, + currentTheme, +}) { + function onHandleRemove() { + handleRemove(todo.id); + } + function onHandleEdit() { + handleEdit(todo.id); + } + + const mainTodoClasses = classNames({ + main__todo: true, + main__todo__darkMode: currentTheme, + }); + + const completedTask = classNames({ + completed: todo.complete, + darkMode: currentTheme, + }); + + return ( +
+
+ + {todo.edit ? ( + + ) : ( + + )} +
+ + +
+ ); +} + +export default Todo; diff --git a/src/components/Todo/Todo.scss b/src/components/Todo/Todo.scss new file mode 100644 index 00000000..dfbebc53 --- /dev/null +++ b/src/components/Todo/Todo.scss @@ -0,0 +1,40 @@ +.main__todo { + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + padding: 0 0.7rem; + + &__close { + opacity: 0.7; + display: none; + } + + &__check { + display: flex; + align-items: center; + width: inherit; + + button { + margin: 0; + padding: 0.7rem; + background: transparent; + border: none; + flex-grow: 1; + text-align: start; + } + } +} + +.main__todo:hover .main__todo__close { + display: block; +} + +.main__todo__darkMode { + border-bottom: 1px solid rgba(255, 255, 255, 0.7); +} +.completed { + opacity: 0.5; + text-decoration: line-through; +} diff --git a/src/components/Todo/index.js b/src/components/Todo/index.js new file mode 100644 index 00000000..012972be --- /dev/null +++ b/src/components/Todo/index.js @@ -0,0 +1 @@ +export { default } from "./Todo"; diff --git a/src/components/TodoList/TodoList.js b/src/components/TodoList/TodoList.js new file mode 100644 index 00000000..2664d1ca --- /dev/null +++ b/src/components/TodoList/TodoList.js @@ -0,0 +1,54 @@ +import React from "react"; +// import { v4 as uuidv4 } from "uuid"; +import classNames from "classnames"; + +import AppFooter from "../AppFooter"; +import EmptyTodo from "../EmptyTodo"; +import Todo from "../Todo"; + +import "./TodoList.scss"; + +function TodoList({ + todos = [], + handleRemove, + handleChangeCheck, + handleEdit, + handleEditSubmit, + handleResetEdit, + handleClearCompleted, + currentTheme, +}) { + function printTodos() { + return todos.map((todo) => ( + + )); + } + + const todoListClasses = classNames({ + main: true, + darkMode: currentTheme, + }); + + return ( +
+
{todos.length ? printTodos() : }
+ + +
+ ); +} + +export default TodoList; diff --git a/src/components/TodoList/TodoList.scss b/src/components/TodoList/TodoList.scss new file mode 100644 index 00000000..79f3ae9e --- /dev/null +++ b/src/components/TodoList/TodoList.scss @@ -0,0 +1,47 @@ +.main { + max-width: 500px; + min-height: 492px; + background-color: white; + border-radius: 5px; + box-shadow: 0px 0px 15px -1px rgba(0, 0, 0, 0.2); + display: flex; + flex-direction: column; + justify-content: space-between; + margin: 0 auto; + margin-top: -45px; + + &__empty { + p { + margin: 0.4rem auto; + text-align: center; + } + img { + max-width: 100%; + } + } + + &__footer { + border-top: 1px solid rgba(0, 0, 0, 0.2); + display: flex; + justify-content: space-around; + + &__lwrp { + display: flex; + justify-content: center; + align-items: center; + } + + &__link { + border: none; + background-color: transparent; + padding: 0.4rem; + } + &__linkDarkMode { + color: white; + } + span { + padding: 0.3rem 0.2rem; + opacity: 0.4; + } + } +} diff --git a/src/components/TodoList/index.js b/src/components/TodoList/index.js new file mode 100644 index 00000000..8d439448 --- /dev/null +++ b/src/components/TodoList/index.js @@ -0,0 +1 @@ +export { default } from "./TodoList"; diff --git a/src/constatnts/routes.js b/src/constatnts/routes.js new file mode 100644 index 00000000..c5654cb9 --- /dev/null +++ b/src/constatnts/routes.js @@ -0,0 +1,3 @@ +export const HOME = "/"; +export const ACTIVE = "/active"; +export const COMPLETED = "/completed"; diff --git a/src/img/hero.jpg b/src/img/hero.jpg new file mode 100644 index 00000000..c374a896 Binary files /dev/null and b/src/img/hero.jpg differ diff --git a/src/img/illustration.svg b/src/img/illustration.svg new file mode 100644 index 00000000..20aa77e2 --- /dev/null +++ b/src/img/illustration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/index.js b/src/index.js index 19bb154c..c4f7c38d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom"; +import { BrowserRouter } from "react-router-dom"; import "bootstrap/dist/css/bootstrap.min.css"; @@ -8,7 +9,9 @@ import reportWebVitals from "./reportWebVitals"; ReactDOM.render( - + + + , document.getElementById("root"), ); diff --git a/src/utils/demo-data.js b/src/utils/demo-data.js new file mode 100644 index 00000000..5225edaf --- /dev/null +++ b/src/utils/demo-data.js @@ -0,0 +1,8 @@ +const todos = [ + { id: 1, name: "Brahim Benalia Casas", complete: false }, + { id: 2, name: "Marc Solá Crack", complete: false }, + { id: 3, name: "Brahim Benalia Casas", complete: false }, + { id: 4, name: "Marc Solá Crack", complete: false }, +]; + +export default todos;