From e6f73932faccd4fca040792dc946d7314a230efc Mon Sep 17 00:00:00 2001 From: Pau Costa Date: Sat, 10 Feb 2024 19:11:01 +0100 Subject: [PATCH] :sparkles: Added search view Signed-off-by: Pau Costa --- client/src/app/store.ts | 2 + client/src/app/usersSlice.ts | 116 ++++++++++++++++++++++++ client/src/components/appAvatar.tsx | 15 ++++ client/src/components/topAppBar.tsx | 134 ++++++++++++++-------------- client/src/routes/search.tsx | 78 +++++++++++++++- server/src/entity/User.ts | 2 +- 6 files changed, 278 insertions(+), 69 deletions(-) create mode 100644 client/src/app/usersSlice.ts create mode 100644 client/src/components/appAvatar.tsx diff --git a/client/src/app/store.ts b/client/src/app/store.ts index 089d197..fb13f32 100644 --- a/client/src/app/store.ts +++ b/client/src/app/store.ts @@ -2,11 +2,13 @@ import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit"; import { useDispatch } from "react-redux"; import loginReducer from "./loginSlice"; import postReducer from "./postSlice"; +import usersReducer from "./usersSlice"; export const store = configureStore({ reducer: { login: loginReducer, post: postReducer, + users: usersReducer, }, }); diff --git a/client/src/app/usersSlice.ts b/client/src/app/usersSlice.ts new file mode 100644 index 0000000..71a1494 --- /dev/null +++ b/client/src/app/usersSlice.ts @@ -0,0 +1,116 @@ +import { Store, createSlice } from "@reduxjs/toolkit"; +import { Configuration, User, UserWithRelations, UsersApi } from "../api"; +import { Status } from "../util/types"; +import { AppThunk, store } from "./store"; + +interface userState { + status: Status; + error: string | null; + users: User[]; + userWithRelations: UserWithRelations | null; +} + +const initialState: userState = { + status: Status.idle, + error: null, + users: [], + userWithRelations: null, +}; + +export const usersSlice = createSlice({ + name: "users", + initialState, + reducers: { + setUsers: (state, action) => { + state.users = action.payload; + }, + setStatus: (state, action) => { + state.status = action.payload; + }, + setError: (state, action) => { + state.error = action.payload; + }, + setUserWithRelations: (state, action) => { + state.userWithRelations = action.payload; + }, + }, +}); + +export const getUsers = (): AppThunk => async (dispatch) => { + const api = createApi(store); + + dispatch(setStatus(Status.loading)); + try { + const response = await api.usersGet(); + dispatch(setUsers(response.data)); + dispatch(setStatus(Status.idle)); + } catch (error) { + dispatch(setError((error as Error).message)); + dispatch(setStatus(Status.idle)); + } +}; + +export const getUserWithRelations = + (userId: number): AppThunk => + async (dispatch) => { + const api = createApi(store); + + dispatch(setStatus(Status.loading)); + try { + const response = await api.usersIdGet(userId); + dispatch(setUserWithRelations(response)); + dispatch(setStatus(Status.idle)); + } catch (error) { + dispatch(setError((error as Error).message)); + dispatch(setStatus(Status.idle)); + } + }; + +export const followUser = + (userId: number): AppThunk => + async (dispatch) => { + const api = createApi(store); + + dispatch(setStatus(Status.loading)); + try { + await api.usersIdFollowPost(userId); + dispatch(getUsers()); + } catch (error) { + dispatch(setError((error as Error).message)); + dispatch(setStatus(Status.idle)); + } + }; + +export const unFollowUser = + (userId: number): AppThunk => + async (dispatch) => { + const api = createApi(store); + + dispatch(setStatus(Status.loading)); + try { + await api.usersIdFollowDelete(userId); + dispatch(getUsers()); + } catch (error) { + dispatch(setError((error as Error).message)); + dispatch(setStatus(Status.idle)); + } + }; + +export const { setUsers, setStatus, setError, setUserWithRelations } = + usersSlice.actions; + +export default usersSlice.reducer; + +export const selectUsers = (state: { users: userState }) => state.users.users; +export const selectAUser = (state: { users: userState }) => + state.users.userWithRelations; +export const selectStatus = (state: { users: userState }) => state.users.status; +export const selectError = (state: { users: userState }) => state.users.error; + +function createApi(store: Store) { + const configuration = new Configuration({ + basePath: process.env.REACT_APP_BACKEND_URL, + accessToken: store.getState().login.userInfo.jwt, + }); + return new UsersApi(configuration); +} diff --git a/client/src/components/appAvatar.tsx b/client/src/components/appAvatar.tsx new file mode 100644 index 0000000..43b2fd6 --- /dev/null +++ b/client/src/components/appAvatar.tsx @@ -0,0 +1,15 @@ +import { Avatar } from "@mui/material"; +import { User, UserWithRelations } from "../api"; + +interface AppAvatarProps { + user: User | UserWithRelations; +} + +export function AppAvatar(props: AppAvatarProps) { + return ( + + ); +} diff --git a/client/src/components/topAppBar.tsx b/client/src/components/topAppBar.tsx index 4d8ced4..2f382ed 100644 --- a/client/src/components/topAppBar.tsx +++ b/client/src/components/topAppBar.tsx @@ -5,13 +5,13 @@ import Toolbar from "@mui/material/Toolbar"; import IconButton from "@mui/material/IconButton"; import Typography from "@mui/material/Typography"; import Menu from "@mui/material/Menu"; -import Avatar from "@mui/material/Avatar"; import Tooltip from "@mui/material/Tooltip"; import MenuItem from "@mui/material/MenuItem"; import NotificationBell from "./notificationBell"; import {useSelector} from "react-redux"; import { postLogout, selectUserInfo } from "../app/loginSlice"; import { useAppDispatch } from "../app/store"; +import { AppAvatar } from "./appAvatar"; interface TopAppBarProps { height: number; @@ -39,73 +39,73 @@ function TopAppBar(props: TopAppBarProps) { }; return ( - - - - DevSpace - - - - + + + + DevSpace + + + + - - - - - - - - - {"Profile"} - - - {"Settings"} - - - {"Logout"} - - - - + + + + + + + + + {"Profile"} + + + {"Settings"} + + + {"Logout"} + + + + ); } diff --git a/client/src/routes/search.tsx b/client/src/routes/search.tsx index 935588d..226ec84 100644 --- a/client/src/routes/search.tsx +++ b/client/src/routes/search.tsx @@ -1,3 +1,79 @@ +import { useSelector } from "react-redux"; +import { getUsers, selectUsers } from "../app/usersSlice"; +import React, { MouseEventHandler, useEffect } from "react"; +import { useAppDispatch } from "../app/store"; +import { + Box, + Button, + Divider, + List, + ListItem, + ListItemText, + Paper, + TextField, +} from "@mui/material"; +import { AppAvatar } from "../components/appAvatar"; +import { useNavigate } from "react-router-dom"; +import { matchSorter } from "match-sorter"; + export default function Search() { - return <>Search; + const userList = useSelector(selectUsers); + const [displayUserList, setDisplayUserList] = React.useState(userList); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + + useEffect(() => { + dispatch(getUsers()); + }, []); + + const handleSearchChange = (event: React.ChangeEvent) => { + const searchValue = event.currentTarget.value; + if (searchValue === "") { + setDisplayUserList(userList); + return; + } + setDisplayUserList( + matchSorter(userList, searchValue, { keys: ["firstName", "lastName"] }) + ); + }; + + const handleClick: MouseEventHandler = (event) => { + const userId = event.currentTarget.id; + navigate(`/user/${userId}`); + }; + + return ( + + + + + + + {displayUserList.map((user) => ( + <> + + + + + + ))} + + + + ); } diff --git a/server/src/entity/User.ts b/server/src/entity/User.ts index 91e1866..2c06bf2 100644 --- a/server/src/entity/User.ts +++ b/server/src/entity/User.ts @@ -21,7 +21,7 @@ export class User { @Column() password: string; - @Column({ default: true }) + @Column({ default: false }) isPrivate: boolean; @Column({ default: null })