Added search view

Signed-off-by: Pau Costa <mico@micodev.es>
main
Pau Costa Ferrer 2024-02-10 19:11:01 +01:00
parent fda05c661a
commit e6f73932fa
6 changed files with 278 additions and 69 deletions

View File

@ -2,11 +2,13 @@ import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import loginReducer from "./loginSlice"; import loginReducer from "./loginSlice";
import postReducer from "./postSlice"; import postReducer from "./postSlice";
import usersReducer from "./usersSlice";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
login: loginReducer, login: loginReducer,
post: postReducer, post: postReducer,
users: usersReducer,
}, },
}); });

View File

@ -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);
}

View File

@ -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 (
<Avatar
alt={`${props.user.firstName} ${props.user.lastName}`}
src={`/images/${props.user.profilePictureId}`}
/>
);
}

View File

@ -5,13 +5,13 @@ import Toolbar from "@mui/material/Toolbar";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import Avatar from "@mui/material/Avatar";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import NotificationBell from "./notificationBell"; import NotificationBell from "./notificationBell";
import {useSelector} from "react-redux"; import {useSelector} from "react-redux";
import { postLogout, selectUserInfo } from "../app/loginSlice"; import { postLogout, selectUserInfo } from "../app/loginSlice";
import { useAppDispatch } from "../app/store"; import { useAppDispatch } from "../app/store";
import { AppAvatar } from "./appAvatar";
interface TopAppBarProps { interface TopAppBarProps {
height: number; height: number;
@ -39,73 +39,73 @@ function TopAppBar(props: TopAppBarProps) {
}; };
return ( return (
<AppBar position="absolute" sx={{zIndex:1600, height: `${props.height}px` }}> <AppBar
<Toolbar disableGutters> position="absolute"
<Typography sx={{ zIndex: 1600, height: `${props.height}px` }}
variant="h6" >
noWrap <Toolbar disableGutters>
component="a" <Typography
sx={{ variant="h6"
marginLeft: "1rem", noWrap
mr: 2, component="a"
display: "flex", sx={{
fontFamily: "monospace", marginLeft: "1rem",
fontWeight: 700, mr: 2,
letterSpacing: ".3rem", display: "flex",
color: "inherit", fontFamily: "monospace",
textDecoration: "none", fontWeight: 700,
}} letterSpacing: ".3rem",
> color: "inherit",
DevSpace textDecoration: "none",
</Typography> }}
<Box >
sx={{ DevSpace
flexGrow: 1, </Typography>
display: "flex", <Box
justifyContent: "right", sx={{
padding: "1rem", flexGrow: 1,
}} display: "flex",
> justifyContent: "right",
<NotificationBell /> padding: "1rem",
</Box> }}
>
<NotificationBell />
</Box>
<Box sx={{ flexGrow: 0, mr:"2rem" }}> <Box sx={{ flexGrow: 0, mr: "2rem" }}>
<Tooltip title="Open settings" > <Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}> <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar <AppAvatar user={userInfo} />
alt={`${userInfo.firstName} ${userInfo.lastName}`} </IconButton>
src="/static/images/avatar/2.jpg" </Tooltip>
/> <Menu
</IconButton> sx={{ mt: "45px" }}
</Tooltip> id="menu-appbar"
<Menu anchorEl={anchorElUser}
sx={{ mt: "45px" }} anchorOrigin={{
id="menu-appbar" vertical: "top",
anchorEl={anchorElUser} horizontal: "right",
anchorOrigin={{ }}
vertical: "top", keepMounted
horizontal: "right", transformOrigin={{
}} vertical: "top",
keepMounted horizontal: "right",
transformOrigin={{ }}
vertical: "top", open={Boolean(anchorElUser)}
horizontal: "right", onClose={handleCloseUserMenu}
}} >
open={Boolean(anchorElUser)} <MenuItem key={"profile"} onClick={handleCloseUserMenu}>
onClose={handleCloseUserMenu} <Typography textAlign="center">{"Profile"}</Typography>
> </MenuItem>
<MenuItem key={"profile"} onClick={handleCloseUserMenu}> <MenuItem key={"settings"} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{"Profile"}</Typography> <Typography textAlign="center">{"Settings"}</Typography>
</MenuItem> </MenuItem>
<MenuItem key={"settings"} onClick={handleCloseUserMenu}> <MenuItem key={"logout"} onClick={handleLogout}>
<Typography textAlign="center">{"Settings"}</Typography> <Typography textAlign="center">{"Logout"}</Typography>
</MenuItem> </MenuItem>
<MenuItem key={"logout"} onClick={handleLogout}> </Menu>
<Typography textAlign="center">{"Logout"}</Typography> </Box>
</MenuItem> </Toolbar>
</Menu>
</Box>
</Toolbar>
</AppBar> </AppBar>
); );
} }

View File

@ -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() { 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<HTMLInputElement>) => {
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 (
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Paper sx={{ mb: "2rem" }}>
<TextField
sx={{ width: "100%" }}
label="Search for a user..."
onChange={handleSearchChange}
/>
</Paper>
<Paper>
<List>
{displayUserList.map((user) => (
<>
<ListItem key={user.id}>
<Button
fullWidth
sx={{ textAlign: "start" }}
id={user.id?.toString()}
onClick={handleClick}
>
<AppAvatar user={user} />
<ListItemText
sx={{ ml: "2rem" }}
primary={`${user.firstName} ${user.lastName}`}
/>
</Button>
</ListItem>
<Divider sx={{ mb: "0.3rem" }} />
</>
))}
</List>
</Paper>
</Box>
);
} }

View File

@ -21,7 +21,7 @@ export class User {
@Column() @Column()
password: string; password: string;
@Column({ default: true }) @Column({ default: false })
isPrivate: boolean; isPrivate: boolean;
@Column({ default: null }) @Column({ default: null })