🏗️ Views and routes boilerplate

Signed-off-by: Pau Costa <mico@micodev.es>
pull/2/head
Pau Costa Ferrer 2024-02-08 16:56:47 +01:00
parent 31ba7bead1
commit 591dae9567
14 changed files with 287 additions and 29 deletions

View File

@ -5,9 +5,9 @@ import {
AuthSignupPostRequest, AuthSignupPostRequest,
AuthenticationApi, AuthenticationApi,
Configuration, Configuration,
UsersApi,
} from "../api"; } from "../api";
import { AppThunk } from "./store"; import { AppThunk } from "./store";
import { Axios, AxiosError } from "axios";
interface loginState { interface loginState {
loggedIn: boolean; loggedIn: boolean;
@ -15,6 +15,7 @@ interface loginState {
error: string | null; error: string | null;
userInfo: { userInfo: {
firstName: string; firstName: string;
lastName: string;
jwt: string; jwt: string;
}; };
} }
@ -25,6 +26,7 @@ const initialState: loginState = {
error: null, error: null,
userInfo: { userInfo: {
firstName: "", firstName: "",
lastName: "",
jwt: "", jwt: "",
}, },
}; };
@ -35,7 +37,9 @@ export const loginSlice = createSlice({
reducers: { reducers: {
login: (state, action) => { login: (state, action) => {
state.loggedIn = true; state.loggedIn = true;
state.userInfo.jwt = action.payload; state.userInfo.jwt = action.payload.jwt;
state.userInfo.firstName = action.payload.firstName;
state.userInfo.lastName = action.payload.lastName;
}, },
logoff: (state) => { logoff: (state) => {
state.loggedIn = false; state.loggedIn = false;
@ -50,7 +54,7 @@ export const loginSlice = createSlice({
}, },
}); });
const api = new AuthenticationApi( const authApi = new AuthenticationApi(
new Configuration({ new Configuration({
basePath: process.env.REACT_APP_BACKEND_URL, basePath: process.env.REACT_APP_BACKEND_URL,
}) })
@ -59,15 +63,30 @@ const api = new AuthenticationApi(
export const postLogin = export const postLogin =
(params: AuthLoginPostRequest): AppThunk => (params: AuthLoginPostRequest): AppThunk =>
async (dispatch) => { async (dispatch) => {
let response; let response, userResponse;
try { try {
dispatch(setStatus(Status.loading)); dispatch(setStatus(Status.loading));
response = await api.authLoginPost(params); response = await authApi.authLoginPost(params);
dispatch(login(response.data.token));
await addJWT(response.data.token || ""); await addJWT(response.data.token || "");
dispatch(setError("")); // Get user info
const userApi = new UsersApi(
new Configuration({
basePath: process.env.REACT_APP_BACKEND_URL,
accessToken: response.data.token,
})
);
userResponse = await userApi.usersMeGet();
dispatch(
login({
jwt: response.data.token,
firstName: userResponse.data.firstName,
lastName: userResponse.data.lastName,
})
);
dispatch(setStatus(Status.succeeded)); dispatch(setStatus(Status.succeeded));
} catch (error) { } catch (error) {
dispatch(setStatus(Status.failed)); dispatch(setStatus(Status.failed));
@ -83,7 +102,7 @@ export const postSignup =
console.log(params); console.log(params);
try { try {
dispatch(setStatus(Status.loading)); dispatch(setStatus(Status.loading));
response = await api.authSignupPost(params); response = await authApi.authSignupPost(params);
dispatch(postLogin({ email: params.email, password: params.password })); dispatch(postLogin({ email: params.email, password: params.password }));
} catch (error) { } catch (error) {
@ -97,7 +116,7 @@ export const postLogout = (): AppThunk => async (dispatch) => {
localStorage.removeItem("jwt"); localStorage.removeItem("jwt");
sessionStorage.removeItem("jwt"); sessionStorage.removeItem("jwt");
dispatch(logoff()); dispatch(logoff());
await api.authLogoutGet(); await authApi.authLogoutGet();
}; };
const addJWT = async (token: string) => { const addJWT = async (token: string) => {

View File

@ -0,0 +1,68 @@
import { Store, createSlice } from "@reduxjs/toolkit";
import { Configuration, Post, PostsApi } from "../api";
import { Status } from "../util/types";
import { store } from "./store";
interface postSlice {
status: Status;
followedPosts: Post[];
globalPosts: Post[];
}
const initialState: postSlice = {
status: Status.idle,
followedPosts: [],
globalPosts: [],
};
export const postSlice = createSlice({
name: "post",
initialState,
reducers: {
setStatus: (state, action) => {
state.status = action.payload;
},
setFollowedPosts: (state, action) => {
state.followedPosts = action.payload;
},
setGlobalPosts: (state, action) => {
state.globalPosts = action.payload;
},
},
});
export const fetchFollowedPosts = () => async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
const response = await postApi.getFollowedPosts();
dispatch(setFollowedPosts(response.data));
dispatch(setStatus(Status.idle));
};
export const fetchGlobalPosts = () => async (dispatch: any) => {
const postApi = createApi(store);
dispatch(setStatus(Status.loading));
const response = await postApi.getAllPosts();
dispatch(setGlobalPosts(response.data));
dispatch(setStatus(Status.idle));
};
export const { setFollowedPosts, setGlobalPosts, setStatus } =
postSlice.actions;
export default postSlice.reducer;
export const selectFollowedPosts = (state: any) => state.post.followedPosts;
export const selectAllPosts = (state: any) => state.post.globalPosts;
export const selectStatus = (state: any) => state.post.status;
function createApi(store: Store) {
return new PostsApi(
new Configuration({
basePath: process.env.REACT_APP_BACKEND_URL,
accessToken: store.getState().login.userInfo.jwt,
})
);
}

View File

@ -1,10 +1,12 @@
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit"; 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";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
login: loginReducer, login: loginReducer,
post: postReducer,
}, },
}); });

View File

@ -4,10 +4,15 @@ import FeedIcon from "@mui/icons-material/Feed";
import GlobalIcon from "@mui/icons-material/Public"; import GlobalIcon from "@mui/icons-material/Public";
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import { useNavigate } from "react-router-dom";
export default function BottomAppBar() { export default function BottomAppBar() {
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
const navigate = useNavigate();
const handleClick = (to: string) => () => {
navigate(to);
};
return ( return (
<Paper <Paper
sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }} sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}
@ -20,10 +25,26 @@ export default function BottomAppBar() {
setValue(newValue); setValue(newValue);
}} }}
> >
<BottomNavigationAction label="My Feed" icon={<FeedIcon />} /> <BottomNavigationAction
<BottomNavigationAction label="Global Feed" icon={<GlobalIcon />} /> label="My Feed"
<BottomNavigationAction label="New Post" icon={<AddIcon />} /> icon={<FeedIcon />}
<BottomNavigationAction label="Search" icon={<SearchIcon />} /> onClick={handleClick("feed")}
/>
<BottomNavigationAction
label="Global Feed"
icon={<GlobalIcon />}
onClick={handleClick("global")}
/>
<BottomNavigationAction
label="New Post"
icon={<AddIcon />}
onClick={handleClick("newpost")}
/>
<BottomNavigationAction
label="Search"
icon={<SearchIcon />}
onClick={handleClick("search")}
/>
</BottomNavigation> </BottomNavigation>
</Paper> </Paper>
); );

View File

@ -0,0 +1,19 @@
import { ListItem, ListItemText } from "@mui/material";
import { Post } from "../api";
interface PostListItemProps {
post: Post;
}
export default function PostListItem(props: PostListItemProps) {
console.log(props.post);
return (
<ListItem>
<ListItemText
primary={props.post.title}
secondary={`${props.post.createdBy?.firstName} ${props.post.createdBy?.lastName}`}
/>
</ListItem>
);
}

View File

@ -13,6 +13,7 @@ import FeedIcon from "@mui/icons-material/Feed";
import GlobalIcon from "@mui/icons-material/Public"; import GlobalIcon from "@mui/icons-material/Public";
import AddIcon from "@mui/icons-material/Add"; import AddIcon from "@mui/icons-material/Add";
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import { Link, useNavigate } from "react-router-dom";
export interface SideAppBarProps { export interface SideAppBarProps {
drawerWidth: number; drawerWidth: number;
@ -20,6 +21,12 @@ export interface SideAppBarProps {
export default function SideAppBar(props: SideAppBarProps) { export default function SideAppBar(props: SideAppBarProps) {
const { drawerWidth } = props; const { drawerWidth } = props;
const navigate = useNavigate();
const handleClick = (to: string) => () => {
navigate(to);
};
return ( return (
<Drawer <Drawer
sx={{ sx={{
@ -37,7 +44,7 @@ export default function SideAppBar(props: SideAppBarProps) {
<Divider /> <Divider />
<List> <List>
<ListItem key={"myfeed"} disablePadding> <ListItem key={"myfeed"} disablePadding onClick={handleClick("feed")}>
<ListItemButton> <ListItemButton>
<ListItemIcon> <ListItemIcon>
<FeedIcon /> <FeedIcon />
@ -46,7 +53,11 @@ export default function SideAppBar(props: SideAppBarProps) {
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem key={"globalfeed"} disablePadding> <ListItem
key={"globalfeed"}
disablePadding
onClick={handleClick("global")}
>
<ListItemButton> <ListItemButton>
<ListItemIcon> <ListItemIcon>
<GlobalIcon /> <GlobalIcon />
@ -55,7 +66,11 @@ export default function SideAppBar(props: SideAppBarProps) {
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem key={"newpost"} disablePadding> <ListItem
key={"newpost"}
disablePadding
onClick={handleClick("newpost")}
>
<ListItemButton> <ListItemButton>
<ListItemIcon> <ListItemIcon>
<AddIcon /> <AddIcon />
@ -64,7 +79,7 @@ export default function SideAppBar(props: SideAppBarProps) {
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<ListItem key={"search"} disablePadding> <ListItem key={"search"} disablePadding onClick={handleClick("search")}>
<ListItemButton> <ListItemButton>
<ListItemIcon> <ListItemIcon>
<SearchIcon /> <SearchIcon />
@ -72,7 +87,6 @@ export default function SideAppBar(props: SideAppBarProps) {
<ListItemText primary={"Search"} /> <ListItemText primary={"Search"} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
</List> </List>
</Drawer> </Drawer>
); );

View File

@ -11,18 +11,21 @@ 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 {selectUserInfo} from "../app/loginSlice"; import { postLogout, selectUserInfo } from "../app/loginSlice";
import { useAppDispatch } from "../app/store";
import { useNavigate } from "react-router-dom";
const settings = ["Profile", "Account", "Dashboard", "Logout"]; const settings = ["Profile", "Account", "Dashboard", "Logout"];
function TopAppBar() { function TopAppBar() {
const dispatch = useAppDispatch();
const userInfo = useSelector(selectUserInfo); const navigate = useNavigate();
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>( const userInfo = useSelector(selectUserInfo);
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
null null
); );
console.log(userInfo) console.log(userInfo);
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => { const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUser(event.currentTarget); setAnchorElUser(event.currentTarget);
@ -32,6 +35,11 @@ function TopAppBar() {
setAnchorElUser(null); setAnchorElUser(null);
}; };
const handleLogout = () => {
dispatch(postLogout());
setAnchorElUser(null);
};
return ( return (
<AppBar position="static"> <AppBar position="static">
<Container maxWidth="xl" sx={{ zIndex: 2500 }}> <Container maxWidth="xl" sx={{ zIndex: 2500 }}>
@ -67,7 +75,10 @@ function TopAppBar() {
<Box sx={{ flexGrow: 0 }}> <Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings"> <Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}> <IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar alt={userInfo.firstName} src="/static/images/avatar/2.jpg" /> <Avatar
alt={`${userInfo.firstName} ${userInfo.lastName}`}
src="/static/images/avatar/2.jpg"
/>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Menu <Menu
@ -86,11 +97,15 @@ function TopAppBar() {
open={Boolean(anchorElUser)} open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu} onClose={handleCloseUserMenu}
> >
{settings.map((setting) => ( <MenuItem key={"profile"} onClick={handleCloseUserMenu}>
<MenuItem key={setting} onClick={handleCloseUserMenu}> <Typography textAlign="center">{"Profile"}</Typography>
<Typography textAlign="center">{setting}</Typography> </MenuItem>
</MenuItem> <MenuItem key={"settings"} onClick={handleCloseUserMenu}>
))} <Typography textAlign="center">{"Settings"}</Typography>
</MenuItem>
<MenuItem key={"logout"} onClick={handleLogout}>
<Typography textAlign="center">{"Logout"}</Typography>
</MenuItem>
</Menu> </Menu>
</Box> </Box>
</Toolbar> </Toolbar>

View File

@ -12,6 +12,11 @@ import Register from "./routes/Auth/register";
import AuthRoot from "./routes/Auth/authRoot"; import AuthRoot from "./routes/Auth/authRoot";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import { store } from "./app/store"; import { store } from "./app/store";
import PostList from "./routes/postList";
import Profile from "./routes/profile";
import Post from "./routes/post";
import NewPost from "./routes/newPost";
import Search from "./routes/search";
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement document.getElementById("root") as HTMLElement
@ -28,6 +33,36 @@ const router = createBrowserRouter([
path: "/", path: "/",
element: <Root />, element: <Root />,
errorElement: <ErrorPage />, errorElement: <ErrorPage />,
children: [
{
path: "feed",
element: <PostList type="user" />,
},
{
path: "global",
element: <PostList type="all" />,
},
{
path: "me",
element: <Profile selfProfile />,
},
{
path: "user/:id",
element: <Profile />,
},
{
path: "post/:id",
element: <Post />,
},
{
path: "search",
element: <Search />,
},
{
path: "newpost",
element: <NewPost />,
},
],
}, },
{ {
path: "/auth", path: "/auth",

View File

@ -0,0 +1,3 @@
export default function NewPost() {
return <>New Post</>;
}

View File

@ -0,0 +1,3 @@
export default function Notification() {
return <>Notification</>;
}

View File

@ -0,0 +1,3 @@
export default function Post() {
return <>Post</>;
}

View File

@ -0,0 +1,41 @@
import { useSelector } from "react-redux";
import {
fetchFollowedPosts,
fetchGlobalPosts,
selectAllPosts,
selectFollowedPosts,
} from "../app/postSlice";
import { store, useAppDispatch } from "../app/store";
import { useEffect } from "react";
import { List, ListItem, ListItemText } from "@mui/material";
import { Post } from "../api";
import PostListItem from "../components/postListItem";
interface PostListProps {
type: "all" | "user";
}
export default function PostList(props: PostListProps) {
const dispatch = useAppDispatch();
const followedPosts = useSelector(selectFollowedPosts);
const globalPosts = useSelector(selectAllPosts);
useEffect(() => {
if (props.type === "all") dispatch(fetchGlobalPosts());
if (props.type === "user") dispatch(fetchFollowedPosts());
}, []);
console.log(globalPosts);
return (
<>
{props.type === "all" && (
<List>
{globalPosts.map((post: Post) => (
<PostListItem post={post} key={post.id} />
))}
</List>
)}
{props.type === "user" && <List></List>}
</>
);
}

View File

@ -0,0 +1,12 @@
import { useParams } from "react-router-dom";
export interface ProfileProps {
selfProfile?: boolean;
}
export default function Profile(props: ProfileProps) {
const { selfProfile } = props;
const userId = useParams<{ userId: string }>().userId;
return <>Profile</>;
}

View File

@ -0,0 +1,3 @@
export default function Search() {
return <>Search</>;
}