Skip to content

Commit

Permalink
Merge pull request #14 from 201flaviosilva/11-remove
Browse files Browse the repository at this point in the history
#11 and #12
  • Loading branch information
201flaviosilva authored May 11, 2023
2 parents 4452f32 + 3c1f64e commit 0253130
Show file tree
Hide file tree
Showing 20 changed files with 415 additions and 113 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ My goal with this project is to learn how to use multiple FE technologies togeth

- [x] Login/sign up/guest mode system;
- [x] Create a new task;
- [ ] Search the list;
- [x] Search the list;
- [ ] Remove:
- [ ] Individually;
- [x] Individually;
- [ ] All;
- [ ] Completed;
- [ ] Repeated;
- [x] Save in local Storage;
- [ ] Favorites;
- [x] Favorites;
- [x] Individual changes:
- [x] Mark as completed;
- [x] Mark as favorite;
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.10.0",
"react-router-dom": "^6.11.0",
"redux": "^4.2.1",
"styled-components": "^5.3.10",
"sweetalert2": "^11.7.3"
Expand All @@ -48,8 +48,9 @@
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-styled-components-a11y": "^2.0.0",
"eslint-plugin-styled-components-a11y": "^2.0.1",
"jest": "^29.5.0",
"np": "^7.7.0",
"source-map-explorer": "^2.5.3",
"typescript": "^5.0.4",
"vite": "^4.3.3",
Expand Down
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { store } from "./app/store";
import Footer from "./components/Footer";
import Header from "./components/Header";
import Main from "./components/Main";
import { PAGES } from "./ENUMS";
import Account from "./pages/Account";
import Home from "./pages/Home";
import NotFound from "./pages/NotFound";
import { PAGES } from "./types/enums";

export default function App() {
return (
Expand Down
64 changes: 52 additions & 12 deletions src/actions/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createSlice, createSelector } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
import { DEFAULT_USERS_IDS, LOCAL_STORAGE } from "../ENUMS";
import { generateUniqueId } from "../utils";
import { LOCAL_STORAGE } from "../types";
import { generateUniqueId, removeDuplicates } from "../utils";
import { getCurrentUserLS, getTasksListByUserID } from "./utils";

export type TaskProp = {
title: string;
Expand Down Expand Up @@ -30,7 +31,10 @@ const initialState: UsersState = {
};

// Setup local storage
localStorage.setItem(LOCAL_STORAGE.TASKS, JSON.stringify(initialState._tasks));
updateTasksStorage(initialState._tasks);
function updateTasksStorage(tasks: Task[]) {
localStorage.setItem(LOCAL_STORAGE.TASKS, JSON.stringify(tasks));
}

const tasksSlice = createSlice({
name: "users",
Expand All @@ -39,9 +43,7 @@ const tasksSlice = createSlice({
addNewTask(state, action: PayloadAction<TaskProp>) {
state._tasks.push({
id: generateUniqueId(),
userID: JSON.parse(
localStorage.getItem(LOCAL_STORAGE.CURRENT_USER) as string
),
userID: getCurrentUserLS(),
...action.payload,
});

Expand All @@ -68,14 +70,51 @@ const tasksSlice = createSlice({
// eslint-disable-next-line no-unused-vars
(task as { [_ in keyof TaskProp]: string | boolean })[prop] = value; // Same as: task[prop] = value;

localStorage.setItem(LOCAL_STORAGE.TASKS, JSON.stringify(state._tasks));
updateTasksStorage(state._tasks);
}
},

changeSearchValue(state, action: PayloadAction<{ value: string }>) {
state.searchValue = action.payload.value;
},

removeTask(state, action: PayloadAction<{ id: string }>) {
const { id: taskId } = action.payload;

state._tasks = state._tasks.filter(({ id }) => id !== taskId);
updateTasksStorage(state._tasks);
},

removeDuplicatedTasks(state) {
const currentUserId = getCurrentUserLS();

const userTasks = state._tasks.filter(
({ userID }) => userID === currentUserId
);

state._tasks = removeDuplicates(userTasks, "title");
updateTasksStorage(state._tasks);
},

removeCompletedTasks(state) {
const currentUserId = getCurrentUserLS();

state._tasks = state._tasks.filter(
({ userID, isCompleted }) => !isCompleted && userID === currentUserId
);
updateTasksStorage(state._tasks);
},

removeAllTasks(state) {
const taskIdsList = getTasksListByUserID(
state._tasks,
getCurrentUserLS()
).map(({ id }) => id);

state._tasks = state._tasks.filter(({ id }) => !taskIdsList.includes(id));
updateTasksStorage(state._tasks);
},

clearStatus(state) {
state.status = null;
},
Expand All @@ -85,17 +124,18 @@ const tasksSlice = createSlice({
// Utils
export const selectCurrentUserTasks = createSelector(
(state: { tasks: { _tasks: Task[] } }) => state.tasks._tasks,
() => JSON.parse(localStorage.getItem(LOCAL_STORAGE.CURRENT_USER) as string),
(tasks, currentUserID) =>
currentUserID === DEFAULT_USERS_IDS.ADMIN
? tasks
: tasks.filter((task) => task.userID === currentUserID)
() => getCurrentUserLS(),
getTasksListByUserID
);

export const {
addNewTask,
changeIndividualProp,
changeSearchValue,
removeTask,
removeDuplicatedTasks,
removeCompletedTasks,
removeAllTasks,
clearStatus,
} = tasksSlice.actions;
export default tasksSlice.reducer;
23 changes: 15 additions & 8 deletions src/actions/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createSlice } from "@reduxjs/toolkit";
import { createSlice, createSelector } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
import { DEFAULT_USERS_IDS, LOCAL_STORAGE } from "../ENUMS";
import { DEFAULT_USERS_IDS, LOCAL_STORAGE } from "../types/enums";
import { generateUniqueId } from "../utils";

export type UserProp = {
Expand All @@ -15,7 +15,7 @@ export type User = UserProp & {

interface UsersState {
users: User[];
currentUser: string | number;
currentUserId: string | number;
status: string | null;
}

Expand All @@ -28,7 +28,7 @@ const initialState: UsersState = {
users: localStorage.getItem(LOCAL_STORAGE.USERS)
? JSON.parse(localStorage.getItem(LOCAL_STORAGE.USERS) as string)
: DEFAULT_USERS,
currentUser: localStorage.getItem(LOCAL_STORAGE.CURRENT_USER)
currentUserId: localStorage.getItem(LOCAL_STORAGE.CURRENT_USER)
? JSON.parse(localStorage.getItem(LOCAL_STORAGE.CURRENT_USER) as string)
: DEFAULT_USERS_IDS.GUEST,
status: null,
Expand All @@ -38,7 +38,7 @@ const initialState: UsersState = {
localStorage.setItem(LOCAL_STORAGE.USERS, JSON.stringify(initialState.users));
localStorage.setItem(
LOCAL_STORAGE.CURRENT_USER,
JSON.stringify(initialState.currentUser)
JSON.stringify(initialState.currentUserId)
);

const usersSlice = createSlice({
Expand Down Expand Up @@ -69,7 +69,7 @@ const usersSlice = createSlice({
foundUser.username === action.payload.username &&
foundUser.password === action.payload.password
) {
state.currentUser = foundUser.id;
state.currentUserId = foundUser.id;
localStorage.setItem(
LOCAL_STORAGE.CURRENT_USER,
JSON.stringify(foundUser.id)
Expand All @@ -82,7 +82,7 @@ const usersSlice = createSlice({
},

logOut(state) {
state.currentUser = DEFAULT_USERS_IDS.GUEST;
state.currentUserId = DEFAULT_USERS_IDS.GUEST;
localStorage.setItem(
LOCAL_STORAGE.CURRENT_USER,
JSON.stringify(DEFAULT_USERS_IDS.GUEST)
Expand All @@ -95,5 +95,12 @@ const usersSlice = createSlice({
},
});

export const { createUser, login, clearStatus } = usersSlice.actions;
// Utils
export const getCurrentUserData = createSelector(
(state: { users: { users: User[] } }) => state.users.users,
() => JSON.parse(localStorage.getItem(LOCAL_STORAGE.CURRENT_USER) as string),
(users, currentUserID) => users.find((u) => u.id === currentUserID)
);

export const { createUser, login, logOut, clearStatus } = usersSlice.actions;
export default usersSlice.reducer;
12 changes: 12 additions & 0 deletions src/actions/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Task } from "./tasks";
import { DEFAULT_USERS_IDS, LOCAL_STORAGE } from "../types";

export function getTasksListByUserID(tasks: Task[], currentUserID: string) {
return currentUserID === DEFAULT_USERS_IDS.ADMIN
? tasks
: tasks.filter((task) => task.userID === currentUserID);
}

export function getCurrentUserLS() {
return JSON.parse(localStorage.getItem(LOCAL_STORAGE.CURRENT_USER) as string);
}
48 changes: 6 additions & 42 deletions src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { GoPerson, GoSearch, GoHome } from "react-icons/go";
import { GoHome } from "react-icons/go";
import { Link, useLocation } from "react-router-dom";
import { changeSearchValue } from "../../actions/tasks";
import { useAppDispatch } from "../../app/hooks";
import { PAGES } from "../../ENUMS";
import { Input, StyledHeader } from "./styled";
import { PAGES } from "../../types/enums";
import { Search } from "./components/Search";
import { UserHomePageIcon } from "./components/UserHomePageIcon";
import { StyledHeader } from "./styled";

export default function Header() {
const localization = useLocation();
Expand All @@ -16,7 +15,7 @@ export default function Header() {
{localization.pathname === PAGES.HOME && (
<>
<Search />
<Link to={PAGES.ACCOUNT}><GoPerson size={32} /></Link>
<UserHomePageIcon />
</>
)}

Expand All @@ -29,38 +28,3 @@ export default function Header() {
</StyledHeader>
);
}

function Search() {
const dispatch = useAppDispatch();

const inputRef = useRef<HTMLInputElement>(null);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [searchTask, setSearchTask] = useState("");

const handleSearchIconClick = useCallback(() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}, [inputRef]);

useEffect(() => {
dispatch(changeSearchValue({ value: searchTask }));
}, [dispatch, searchTask]);

return (
<div>
{!isSearchOpen && !searchTask && <GoSearch
onClick={handleSearchIconClick}
size={28}
/>}
<Input
type="search"
placeholder="Search for a task"
ref={inputRef}
isActive={isSearchOpen || !!searchTask}
value={searchTask}
onChange={(e) => setSearchTask(e.target.value)}
onBlur={() => setIsSearchOpen(false)}
/>
</div>
);
}
40 changes: 40 additions & 0 deletions src/components/Header/components/Search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { GoSearch } from "react-icons/go";
import { changeSearchValue } from "../../../actions/tasks";
import { useAppDispatch } from "../../../app/hooks";
import { Input, SearchWrapper } from "../styled";

export function Search() {
const dispatch = useAppDispatch();

const inputRef = useRef<HTMLInputElement>(null);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [searchTask, setSearchTask] = useState("");

const handleSearchIconClick = useCallback(() => {
setIsSearchOpen(true);
inputRef.current?.focus();
}, [inputRef]);

useEffect(() => {
dispatch(changeSearchValue({ value: searchTask }));
}, [dispatch, searchTask]);

return (
<SearchWrapper>
{!isSearchOpen && !searchTask && <GoSearch
onClick={handleSearchIconClick}
size={28}
/>}
<Input
type="search"
placeholder="Search for a task"
ref={inputRef}
isActive={isSearchOpen || !!searchTask}
value={searchTask}
onChange={(e) => setSearchTask(e.target.value)}
onBlur={() => setIsSearchOpen(false)}
/>
</SearchWrapper>
);
}
39 changes: 39 additions & 0 deletions src/components/Header/components/UserHomePageIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useState, useCallback } from "react";
import { GoPerson, GoSignOut, GoSignIn } from "react-icons/go";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { User, logOut, getCurrentUserData } from "../../../actions/users";
import { useAppDispatch } from "../../../app/hooks";
import { PAGES, DEFAULT_USERS_IDS } from "../../../types/enums";
import { IconWrapper } from "../styled";

export function UserHomePageIcon() {
const currentUser: User | undefined = useSelector(getCurrentUserData);
const [isHouver, setIsHouver] = useState(false);
const dispatch = useAppDispatch();

const Icon = useCallback((props: { size: number }) => {
if (isHouver && currentUser) {
if (currentUser.id !== DEFAULT_USERS_IDS.GUEST)
return <GoSignOut
onClick={() => dispatch(logOut())}
title="Log Out"
{...props}
/>;

else return <GoSignIn title="Log In" {...props} />;
} else return <GoPerson {...props} />;
}, [currentUser, dispatch, isHouver]);

return (
<Link
to={PAGES.ACCOUNT}
onMouseEnter={() => setIsHouver(true)}
onMouseLeave={() => setIsHouver(false)}>
<IconWrapper>
<Icon size={32} />
{currentUser && <p>{currentUser.username}</p>}
</IconWrapper>
</Link>
);
}
Loading

0 comments on commit 0253130

Please sign in to comment.