diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:11:50 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-02-20 16:15:08 +0100 |
| commit | 73a5c7fae9ffbe9ada721148c8c454a643aceebe (patch) | |
| tree | c8fad013ed9b5dd589add87f8d45cf02bbfc6e91 /public/projects/react-small-apps/apps/todos/src/views | |
| parent | b01239fbdcc5bbc5921f73ec0e8fee7bedd5c8e8 (diff) | |
chore!: restructure repo
I separated public files from the config/dev files. It improves repo
readability.
I also moved dotenv helper to public/inc directory and extract the
Matomo tracker in the same directory.
Diffstat (limited to 'public/projects/react-small-apps/apps/todos/src/views')
11 files changed, 551 insertions, 0 deletions
diff --git a/public/projects/react-small-apps/apps/todos/src/views/Account/Account.js b/public/projects/react-small-apps/apps/todos/src/views/Account/Account.js new file mode 100644 index 0000000..85aab6b --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/Account/Account.js @@ -0,0 +1,26 @@ +import { useSelector } from "react-redux"; +import { Link } from "react-router-dom"; +import "./Account.scss"; + +function Account() { + const currentUser = useSelector((state) => state.auth.currentUser); + + return ( + <div> + <Link to="/">Back to your todo list</Link> + <h2>Account</h2> + <dl className="account-details"> + <dt className="account-details__headings">Username</dt> + <dd className="account-details__data">{currentUser.username}</dd> + <dt className="account-details__headings">Email</dt> + <dd className="account-details__data">{currentUser.email}</dd> + <dt className="account-details__headings">Creation date</dt> + <dd className="account-details__data"> + {new Date(currentUser.createdAt).toLocaleDateString()} + </dd> + </dl> + </div> + ); +} + +export default Account; diff --git a/public/projects/react-small-apps/apps/todos/src/views/Account/Account.scss b/public/projects/react-small-apps/apps/todos/src/views/Account/Account.scss new file mode 100644 index 0000000..a8b2ba7 --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/Account/Account.scss @@ -0,0 +1,15 @@ +.account-details { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + row-gap: 0.5rem; + column-gap: 1rem; + + &__headings { + grid-column: 1; + font-weight: 600; + } + + &__data { + grid-column: 2; + } +} diff --git a/public/projects/react-small-apps/apps/todos/src/views/LoginForm/LoginForm.js b/public/projects/react-small-apps/apps/todos/src/views/LoginForm/LoginForm.js new file mode 100644 index 0000000..6c90e67 --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/LoginForm/LoginForm.js @@ -0,0 +1,84 @@ +import { useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { Button, Fieldset, Input } from "../../components/forms"; +import { login } from "../../store/auth/auth.slice"; + +function LoginForm() { + const [inputEmailValue, setInputEmailValue] = useState(""); + const [inputPasswordValue, setInputPasswordValue] = useState(""); + const [errorMsg, setErrorMsg] = useState(""); + const usersList = useSelector((state) => state.users); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const getCurrentUser = (email) => { + return usersList.find((user) => user.email === email); + }; + + const isValidUser = (email) => { + const currentUser = getCurrentUser(email); + return currentUser ? true : false; + }; + + const isValidPassword = (currentUser, password) => { + return currentUser.password === password; + }; + + const handleSubmit = (e) => { + e.preventDefault(); + if (isValidUser(inputEmailValue)) { + const currentUser = getCurrentUser(inputEmailValue); + + if (isValidPassword(currentUser, inputPasswordValue)) { + setErrorMsg(""); + dispatch(login(currentUser)); + navigate("/"); + } else { + setErrorMsg("The password does not match."); + } + } else { + setErrorMsg("This email address does not exist."); + } + }; + + const displayError = (msg) => { + return msg ? <p>{msg}</p> : ""; + }; + + return ( + <form + action="#" + method="post" + className="form form--login" + onSubmit={handleSubmit} + > + {displayError(errorMsg)} + <Fieldset legend="Sign In"> + <Input + label="Email" + id="login-email" + name="login-email" + value={inputEmailValue} + updateValue={setInputEmailValue} + type="email" + required + /> + <Input + label="Password" + id="login-password" + name="login-password" + value={inputPasswordValue} + updateValue={setInputPasswordValue} + type="password" + required + /> + <Button type="submit" modifiers={["submit"]}> + Log in + </Button> + </Fieldset> + </form> + ); +} + +export default LoginForm; diff --git a/public/projects/react-small-apps/apps/todos/src/views/Logout/Logout.js b/public/projects/react-small-apps/apps/todos/src/views/Logout/Logout.js new file mode 100644 index 0000000..cf9bbd0 --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/Logout/Logout.js @@ -0,0 +1,18 @@ +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { logout } from "../../store/auth/auth.slice"; + +function Logout() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + + useEffect(() => { + dispatch(logout()); + navigate("/"); + }); + + return <>Logging out...</>; +} + +export default Logout; diff --git a/public/projects/react-small-apps/apps/todos/src/views/Todo/Todo.js b/public/projects/react-small-apps/apps/todos/src/views/Todo/Todo.js new file mode 100644 index 0000000..1160ab5 --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/Todo/Todo.js @@ -0,0 +1,86 @@ +import { useEffect, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Link, useLocation } from "react-router-dom"; +import { updateTodo } from "../../store/todos/todos.slice"; +import useToggle from "../../utilities/hooks"; +import "./Todo.scss"; + +function Todo() { + const [isTitleEditable, setIsTitleEditable] = useToggle(); + const [isBodyEditable, setIsBodyEditable] = useToggle(); + const titleRef = useRef(null); + const bodyRef = useRef(null); + const location = useLocation(); + const todoId = location.state.todoId; + const currentTodo = useSelector((state) => state.todos).find( + (todo) => todo.id === todoId + ); + const { title, body } = currentTodo; + const dispatch = useDispatch(); + + useEffect(() => { + titleRef.current && titleRef.current.focus(); + bodyRef.current && bodyRef.current.focus(); + }); + + const handleSubmit = (e) => { + console.log(e); + }; + + return ( + <> + <Link to="/">Back to your todo list</Link> + <div className="todo"> + {isTitleEditable ? ( + <form + action="#" + method="post" + className="todo-form todo__title" + onSubmit={handleSubmit} + > + <input + ref={titleRef} + className="todo-form__field" + type="text" + name="title" + value={title} + onChange={(e) => + dispatch(updateTodo({ ...currentTodo, title: e.target.value })) + } + onBlur={setIsTitleEditable} + /> + </form> + ) : ( + <div className="todo__title" onClick={setIsTitleEditable}> + {title} + </div> + )} + {isBodyEditable ? ( + <form + action="#" + method="post" + className="todo-form todo__body" + onSubmit={handleSubmit} + > + <textarea + ref={bodyRef} + className="todo-form__field todo-form__field--textarea" + name="body" + value={body} + onChange={(e) => + dispatch(updateTodo({ ...currentTodo, body: e.target.value })) + } + onBlur={setIsBodyEditable} + /> + </form> + ) : ( + <div className="todo__body" onClick={setIsBodyEditable}> + {body} + </div> + )} + </div> + </> + ); +} + +export default Todo; diff --git a/public/projects/react-small-apps/apps/todos/src/views/Todo/Todo.scss b/public/projects/react-small-apps/apps/todos/src/views/Todo/Todo.scss new file mode 100644 index 0000000..67baa2d --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/Todo/Todo.scss @@ -0,0 +1,31 @@ +.todo { + border: 1px solid #000; + margin-top: 2rem; + padding: 1rem; + + & &__title { + font-size: 1.2rem; + } + + & &__body { + margin-top: 1rem; + min-height: 10rem; + } +} + +.todo-form { + &__field { + border: none; + padding: 0; + margin: 0; + + &--textarea { + resize: none; + line-height: inherit; + } + + &:focus { + outline: none; + } + } +} diff --git a/public/projects/react-small-apps/apps/todos/src/views/TodoForm/TodoForm.js b/public/projects/react-small-apps/apps/todos/src/views/TodoForm/TodoForm.js new file mode 100644 index 0000000..bc6ed97 --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/TodoForm/TodoForm.js @@ -0,0 +1,42 @@ +import { useState } from "react"; +import { useDispatch } from "react-redux"; +import { Button, Fieldset, Input, TextArea } from "../../components/forms"; +import { addTodo } from "../../store/todos/todos.slice"; + +function TodoForm({ userId, closeForm }) { + const [titleValue, setTitleValue] = useState(""); + const [bodyValue, setBodyValue] = useState(""); + const dispatch = useDispatch(); + + const handleSubmit = (e) => { + e.preventDefault(); + closeForm((prev) => !prev); + }; + + const handleSave = () => { + const newTodo = { userId, title: titleValue, body: bodyValue }; + dispatch(addTodo(newTodo)); + }; + + return ( + <form onSubmit={handleSubmit} className="form form--todo"> + <Fieldset legend="Add a new todo"> + <Input label="Title" value={titleValue} updateValue={setTitleValue} /> + <TextArea + label="Details" + value={bodyValue} + updateValue={setBodyValue} + /> + <Button + type="submit" + modifiers={["submit"]} + onClickHandler={handleSave} + > + Save + </Button> + </Fieldset> + </form> + ); +} + +export default TodoForm; diff --git a/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoList.js b/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoList.js new file mode 100644 index 0000000..c671459 --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoList.js @@ -0,0 +1,84 @@ +import { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Button } from "../../components/forms"; +import TodoForm from "../TodoForm/TodoForm"; +import TodoListItem from "./TodoListItem"; +import "./TodoList.scss"; +import { deleteAllTodos } from "../../store/todos/todos.slice"; +import { LocalStorage } from "../../services/LocalStorage.service"; +import TodoListFilters from "./TodoListFilters"; + +function TodoList() { + const [todosList, setTodosList] = useState([]); + const [currentView, setCurrentView] = useState("all"); + const dispatch = useDispatch(); + const [isToggled, setIsToggled] = useState(false); + const currentUser = useSelector((state) => state.auth.currentUser); + const allTodos = useSelector((state) => state.todos); + + useEffect(() => { + const userTodos = allTodos.filter((todo) => todo.userId === currentUser.id); + + setTodosList(() => { + let list; + + switch (currentView) { + case "completed": + list = userTodos.filter((todo) => todo.done); + break; + case "ongoing": + list = userTodos.filter((todo) => !todo.done); + break; + default: + list = userTodos; + break; + } + + return list; + }); + }, [currentView, allTodos, currentUser.id]); + + useEffect(() => { + LocalStorage.set("todoList", allTodos); + }); + + const userTodosList = todosList.map((todo) => ( + <TodoListItem key={todo.id} todo={todo} /> + )); + + return ( + <div> + <h2>Welcome back {currentUser.username}!</h2> + <div className="todos-actions"> + <Button + modifiers={["action"]} + onClickHandler={() => setIsToggled(!isToggled)} + > + New todo + </Button> + <Button + modifiers={["action", "delete"]} + onClickHandler={() => dispatch(deleteAllTodos())} + > + Delete all + </Button> + </div> + {isToggled ? ( + <TodoForm userId={currentUser.id} closeForm={setIsToggled} /> + ) : ( + "" + )} + <TodoListFilters + currentView={currentView} + setCurrentView={setCurrentView} + /> + {userTodosList.length > 0 ? ( + <ul className="todos-list">{userTodosList}</ul> + ) : ( + "" + )} + </div> + ); +} + +export default TodoList; diff --git a/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoList.scss b/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoList.scss new file mode 100644 index 0000000..024fe3e --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoList.scss @@ -0,0 +1,63 @@ +@use "../../sass/abstracts/placeholders"; +@use "../../sass/abstracts/variables" as var; + +.todos-actions { + display: flex; + gap: 1rem; +} + +.todos-filters { + display: flex; + flex-flow: row wrap; + align-items: center; + gap: clamp(0.2rem, 1vw, 0.5rem); + margin-top: 1rem +} + +.todos-list { + @extend %list-reset; + border: 1px solid #000; + margin-top: 1.5rem; + + &__item { + display: flex; + flex-flow: row wrap; + align-items: center; + gap: 1rem; + padding: 1rem; + + &:not(:first-child) { + border-top: 1px solid #000; + } + + .form__label { + margin: 0; + letter-spacing: 0; + text-transform: none; + } + + .todo__title { + background-image: linear-gradient( + to top, + transparent calc(50% - 3px), + var.$done-color calc(50% - 3px), + var.$done-color 50%, + transparent 50% + ); + background-size: 0 100%; + background-repeat: no-repeat; + margin-right: auto; + transition: background-size 0.3s ease-in-out 0s; + } + + &--done { + .todo__title { + background-size: 100% 100%; + } + } + + .btn { + padding: 0.4rem 0.7rem; + } + } +} diff --git a/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoListFilters.js b/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoListFilters.js new file mode 100644 index 0000000..338492a --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoListFilters.js @@ -0,0 +1,47 @@ +import { Button } from "../../components/forms"; + +function TodoListFilters({ currentView, setCurrentView }) { + let allModifiers = ["filters"]; + let ongoingModifiers = ["filters"]; + let completedModifiers = ["filters"]; + + switch (currentView) { + case "all": + allModifiers.push("current"); + break; + case "ongoing": + ongoingModifiers.push("current"); + break; + case "completed": + completedModifiers.push("current"); + break; + default: + break; + } + + return ( + <div className="todos-filters"> + Show: + <Button + modifiers={allModifiers} + onClickHandler={() => setCurrentView("all")} + > + All + </Button> + <Button + modifiers={ongoingModifiers} + onClickHandler={() => setCurrentView("ongoing")} + > + Ongoing + </Button> + <Button + modifiers={completedModifiers} + onClickHandler={() => setCurrentView("completed")} + > + Completed + </Button> + </div> + ); +} + +export default TodoListFilters; diff --git a/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoListItem.js b/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoListItem.js new file mode 100644 index 0000000..f76c1a1 --- /dev/null +++ b/public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoListItem.js @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { Link } from "react-router-dom"; +import { Button, Input } from "../../components/forms"; +import { deleteTodo, toggleTodo } from "../../store/todos/todos.slice"; +import { slugify } from "../../utilities/helpers"; + +function TodoListItem({ todo }) { + const { id, createdAt, title, done } = todo; + const [isChecked, setIsChecked] = useState(false); + const dispatch = useDispatch(); + + useEffect(() => { + if (done) setIsChecked(true); + }, [done]); + + const handleTodoDone = (checkboxState) => { + setIsChecked(checkboxState); + dispatch(toggleTodo(id)); + }; + + const todoSlug = slugify(title); + + const classNames = `todos-list__item ${ + isChecked ? "todos-list__item--done" : "" + }`; + + return ( + <li className={classNames}> + <span className="todo__date"> + {new Date(createdAt).toLocaleDateString()} + </span> + <span className="todo__title"> + <Link to={`/todo/${todoSlug}`} state={{ todoId: todo.id }}> + {title} + </Link> + </span> + <Input + type="checkbox" + label="Done?" + id={id} + value={isChecked} + updateValue={handleTodoDone} + /> + <Button + modifiers={["action", "delete"]} + onClickHandler={() => dispatch(deleteTodo(id))} + > + Delete + </Button> + </li> + ); +} + +export default TodoListItem; |
