aboutsummaryrefslogtreecommitdiffstats
path: root/public/projects/react-small-apps/apps/todos/src/views
diff options
context:
space:
mode:
Diffstat (limited to 'public/projects/react-small-apps/apps/todos/src/views')
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/Account/Account.js26
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/Account/Account.scss15
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/LoginForm/LoginForm.js84
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/Logout/Logout.js18
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/Todo/Todo.js86
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/Todo/Todo.scss31
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/TodoForm/TodoForm.js42
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoList.js84
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoList.scss63
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoListFilters.js47
-rw-r--r--public/projects/react-small-apps/apps/todos/src/views/TodoList/TodoListItem.js55
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;