Es un proyecto que tiene como objetivo aplicar teoría de componentes utilizando Atomic Design
bajo el principio de responsabilidad única. Para los estilos se utilizó el preprocesador de CSS SASS
. Se creó la aplicación con Create React App
, se utilizó conceptos de estados de componentes y el ciclo de vida de los mismos, además de varios hooks
. La aplicación también utiliza persistencia de datos en el Local Storage
.
El proyecto esta disponible en:
En la imagen se puede observar la pantalla de inicio, donde se pueden agregar las tareas a realizar.
Se despliega un modal, en el cual se escribe la tarea a realizar y se le da en añadir.
Las tareas se agregan cronológicamente, también se puede indicar que ya fueron completadas mostrando el total de ellas en la parte superior. Del mismo modo se las pueden eliminar para que desaparezcan de la lista. Asimismo, es bueno mencionar que todos los datos son guardados en el local storage de tal modo que si cerramos el navegador los datos persisten para la próxima vez que se abra el navegador e ingrese a la aplicación.
Se pueden ingresar caracteres y/o palabras para filtrar las tareas a mostrar en la lista.
Por ejemplo, si ingreso "im" se desplegaría en la lista de tareas "Implementar estados a mis componentes".
En caso de no encontrar coincidencias, se despliega un mensaje de "No hay resultados para:" el criterio de su búsqueda.
Se construyó utilizando la metodología mobile first
para dispositivos de 375px. Asimismo, para los estilos se hace el uso de la metodología BEM
en el preprocesador SASS
. También se utiliza React
para crear componentes utilizando Atomic Design
y aprovechar los estados de los componentes junto a sus hooks.
A continuación se mostrará algunos detalles y buenas prácticas:
Componentes de la aplicación
<TodoHeader>
<TodoCounter totalTodos={totalTodos} completedTodos={completedTodos} />
<TodoSearch searchValue={searchValue} setSearchValue={setSearchValue} />
</TodoHeader>
<TodoList
filteredText={filteredText}
totalTodos={totalTodos}
searchValue={searchValue}
onEmptyTodos={() => <EmptyTodos />}
onEmptySearchResults={(searchText) => (
<p className="empty-todos">No hay resultados para: {searchText}</p>
)}
render={(todo) => (
<TodoItem
key={todo.text}
text={todo.text}
completed={todo.completed}
onComplete={() => toggleTodo(todo.text)}
onDelete={() => deleteTodo(todo.text)}
/>
)}
>
</TodoList>
{openModal && (
<Modal>
<TodoForm addTodo={addTodo} setOpenModal={setOpenModal} />
</Modal>
)}
<CreateTodoButton openModal={openModal} setOpenModal={setOpenModal} />
<ChangeAlertWithStorageListener sincronize={setSincronizedItem} />
Importación de estilos por componente
//index.scss
@import "./components/styles/globales.scss";
@import "./components/styles/TodoCounter";
@import "./components/styles/CreateTodoButton";
@import "./components/styles/TodoList";
@import "./components/styles/TodoSearch";
@import "./components/styles/TodoItem.scss";
@import "./components/styles/Modal";
@import "./components/styles/TodoForm";
@import "./components/styles/ChangeAlert";
@import "./components/styles/EmptyTodos";
body,
html {
margin: 0;
padding: 0;
box-sizing: border-box;
background: $bg-color;
font-family: $Fuente1;
}
#root {
margin: 0 24px;
min-height: 100vh;
}
Uso de variables
$Fuente1: 'Nunito', sans-serif;
$color-primario: #29A19C;
$color-primario-disabled:#29a19c75;
$color-secundario: #F9F9F9;
$color-secundario-tranparenci:#f9f9f9ad;
$color-variante:rgba(249, 249, 249, 0.2);
$bg-color: #222831;
$bg-component:#2C3440;
Anidamiento en los estilos
.ChangeAlert-bg{
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #1e1e1f50;
z-index: 2;
& .alert-container{
height: 100%;
width: 80%;
margin: 0 auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
& p{
width: 100%;
height: 56px;
margin: 0;
background: #f75858;
color: $color-secundario;
display: flex;
justify-content: center;
align-items: center;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
cursor: pointer;
}
& button{
width: 100%;
height: 48px;
margin: 0;
background: $color-secundario;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
color: $bg-component;
font-family: $Fuente1;
font-size: 16px;
font-weight: 700;
border: 1px solid transparent;
transition: border 0.15;
&:hover{
border: $color-secundario-tranparenci;
}
}
}
}
Custom hook
//useTodos.js
import { useState, useMemo } from "react";
import { useLocalStorage } from "./useLocalStorage";
function useTodos() {
const [todos, saveTodos, setSincronizedItem] = useLocalStorage('TODOS_V1', []);
const [searchValue, setSearchValue] = useState('');
const [openModal, setOpenModal] = useState(false);
const completedTodos = todos.filter(todo => !!todo.completed).length;
const totalTodos = todos.length;
const filteredText = useMemo(() =>
todos.filter((todo) => {
return todo.text.toLowerCase().includes(searchValue.toLowerCase())
}), [todos, searchValue]
)
const toggleTodo = (text) => {
const todoIndex = todos.findIndex(todo => todo.text === text);
const newTodos = [...todos];
newTodos[todoIndex].completed = !todos[todoIndex].completed;
saveTodos(newTodos);
}
const deleteTodo = (text) => {
const todoIndex = todos.findIndex(todo => todo.text === text);
const newTodos = [...todos];
newTodos.splice(todoIndex, 1);
saveTodos(newTodos);
}
const addTodo = (text) => {
const newTodos = [...todos];
newTodos.push({
completed: false,
text,
}
);
saveTodos(newTodos);
}
const states={
totalTodos,
completedTodos,
searchValue,
filteredText,
openModal,
}
const stateUpdaters={
setSearchValue,
toggleTodo,
deleteTodo,
setOpenModal,
addTodo,
setSincronizedItem
}
return {states, stateUpdaters}
}
export { useTodos }
Persistencia de datos en el local storage utilizando useEffect y useReducer
//useLocalStorage.js
import { useEffect, useReducer } from "react"
function useLocalStorage(itemName, initialValue) {
const [state, dispatch] = useReducer(reducer, initialState({ initialValue }));
const {
sincronizedItem,
item
} = state
//Action Creators
const onSuccess = (parsedItem) => dispatch({
type: actionTypes.success,
payload: parsedItem
})
const onSave = (newItem) => dispatch({
type: actionTypes.save,
payload: newItem
})
const onSincronize = ()=> dispatch({
type: actionTypes.sincronize
})
useEffect(() => {
const localStorageItem = localStorage.getItem(itemName);
let parsedItem;
if (!localStorageItem) {
localStorage.setItem(itemName, JSON.stringify(initialValue));
parsedItem = initialValue;
} else {
parsedItem = JSON.parse(localStorageItem);
}
onSuccess(parsedItem)
}, [sincronizedItem])
const saveItem = (newItem) => {
localStorage.setItem(itemName, JSON.stringify(newItem))
onSave(newItem)
}
const sincronizeItem = () => {
onSincronize()
}
return [
item,
saveItem,
sincronizeItem
]
}
const initialState = ({ initialValue }) => ({
sincronizedItem: true,
item: initialValue
})
const actionTypes = {
success: 'SUCCESS',
save: 'SAVE',
sincronize: 'SINCRONIZE'
}
const reducerObject = (state, payload) => ({
[actionTypes.success]: {
...state,
sincronizedItem: true,
item: payload
},
[actionTypes.save]: {
...state,
item: payload
},
[actionTypes.sincronize]: {
...state,
sincronizedItem: false,
}
})
const reducer = (state, action) => {
return reducerObject(state, action.payload)[action.type] || state
}
export { useLocalStorage };
Optimizando las búsquedas con useMemo
const filteredText = useMemo(() =>
todos.filter((todo) => {
return todo.text.toLowerCase().includes(searchValue.toLowerCase())
}), [todos, searchValue]
)
Este proyecto esta bajo la licencia de MIT.
Made with 💜 by ArturoMauricioDev