🏗️ 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,
AuthenticationApi,
Configuration,
UsersApi,
} from "../api";
import { AppThunk } from "./store";
import { Axios, AxiosError } from "axios";
interface loginState {
loggedIn: boolean;
@ -15,6 +15,7 @@ interface loginState {
error: string | null;
userInfo: {
firstName: string;
lastName: string;
jwt: string;
};
}
@ -25,6 +26,7 @@ const initialState: loginState = {
error: null,
userInfo: {
firstName: "",
lastName: "",
jwt: "",
},
};
@ -35,7 +37,9 @@ export const loginSlice = createSlice({
reducers: {
login: (state, action) => {
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) => {
state.loggedIn = false;
@ -50,7 +54,7 @@ export const loginSlice = createSlice({
},
});
const api = new AuthenticationApi(
const authApi = new AuthenticationApi(
new Configuration({
basePath: process.env.REACT_APP_BACKEND_URL,
})
@ -59,15 +63,30 @@ const api = new AuthenticationApi(
export const postLogin =
(params: AuthLoginPostRequest): AppThunk =>
async (dispatch) => {
let response;
let response, userResponse;
try {
dispatch(setStatus(Status.loading));
response = await api.authLoginPost(params);
response = await authApi.authLoginPost(params);
dispatch(login(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));
} catch (error) {
dispatch(setStatus(Status.failed));
@ -83,7 +102,7 @@ export const postSignup =
console.log(params);
try {
dispatch(setStatus(Status.loading));
response = await api.authSignupPost(params);
response = await authApi.authSignupPost(params);
dispatch(postLogin({ email: params.email, password: params.password }));
} catch (error) {
@ -97,7 +116,7 @@ export const postLogout = (): AppThunk => async (dispatch) => {
localStorage.removeItem("jwt");
sessionStorage.removeItem("jwt");
dispatch(logoff());
await api.authLogoutGet();
await authApi.authLogoutGet();
};
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 { useDispatch } from "react-redux";
import loginReducer from "./loginSlice";
import postReducer from "./postSlice";
export const store = configureStore({
reducer: {
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 AddIcon from "@mui/icons-material/Add";
import SearchIcon from "@mui/icons-material/Search";
import { useNavigate } from "react-router-dom";
export default function BottomAppBar() {
const [value, setValue] = useState(0);
const navigate = useNavigate();
const handleClick = (to: string) => () => {
navigate(to);
};
return (
<Paper
sx={{ position: "fixed", bottom: 0, left: 0, right: 0 }}
@ -20,10 +25,26 @@ export default function BottomAppBar() {
setValue(newValue);
}}
>
<BottomNavigationAction label="My Feed" icon={<FeedIcon />} />
<BottomNavigationAction label="Global Feed" icon={<GlobalIcon />} />
<BottomNavigationAction label="New Post" icon={<AddIcon />} />
<BottomNavigationAction label="Search" icon={<SearchIcon />} />
<BottomNavigationAction
label="My Feed"
icon={<FeedIcon />}
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>
</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 AddIcon from "@mui/icons-material/Add";
import SearchIcon from "@mui/icons-material/Search";
import { Link, useNavigate } from "react-router-dom";
export interface SideAppBarProps {
drawerWidth: number;
@ -20,6 +21,12 @@ export interface SideAppBarProps {
export default function SideAppBar(props: SideAppBarProps) {
const { drawerWidth } = props;
const navigate = useNavigate();
const handleClick = (to: string) => () => {
navigate(to);
};
return (
<Drawer
sx={{
@ -37,7 +44,7 @@ export default function SideAppBar(props: SideAppBarProps) {
<Divider />
<List>
<ListItem key={"myfeed"} disablePadding>
<ListItem key={"myfeed"} disablePadding onClick={handleClick("feed")}>
<ListItemButton>
<ListItemIcon>
<FeedIcon />
@ -46,7 +53,11 @@ export default function SideAppBar(props: SideAppBarProps) {
</ListItemButton>
</ListItem>
<ListItem key={"globalfeed"} disablePadding>
<ListItem
key={"globalfeed"}
disablePadding
onClick={handleClick("global")}
>
<ListItemButton>
<ListItemIcon>
<GlobalIcon />
@ -55,7 +66,11 @@ export default function SideAppBar(props: SideAppBarProps) {
</ListItemButton>
</ListItem>
<ListItem key={"newpost"} disablePadding>
<ListItem
key={"newpost"}
disablePadding
onClick={handleClick("newpost")}
>
<ListItemButton>
<ListItemIcon>
<AddIcon />
@ -64,7 +79,7 @@ export default function SideAppBar(props: SideAppBarProps) {
</ListItemButton>
</ListItem>
<ListItem key={"search"} disablePadding>
<ListItem key={"search"} disablePadding onClick={handleClick("search")}>
<ListItemButton>
<ListItemIcon>
<SearchIcon />
@ -72,7 +87,6 @@ export default function SideAppBar(props: SideAppBarProps) {
<ListItemText primary={"Search"} />
</ListItemButton>
</ListItem>
</List>
</Drawer>
);

View File

@ -11,18 +11,21 @@ import Tooltip from "@mui/material/Tooltip";
import MenuItem from "@mui/material/MenuItem";
import NotificationBell from "./notificationBell";
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"];
function TopAppBar() {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const userInfo = useSelector(selectUserInfo);
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
null
);
console.log(userInfo)
console.log(userInfo);
const handleOpenUserMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElUser(event.currentTarget);
@ -32,6 +35,11 @@ function TopAppBar() {
setAnchorElUser(null);
};
const handleLogout = () => {
dispatch(postLogout());
setAnchorElUser(null);
};
return (
<AppBar position="static">
<Container maxWidth="xl" sx={{ zIndex: 2500 }}>
@ -67,7 +75,10 @@ function TopAppBar() {
<Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings">
<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>
</Tooltip>
<Menu
@ -86,11 +97,15 @@ function TopAppBar() {
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
{settings.map((setting) => (
<MenuItem key={setting} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{setting}</Typography>
<MenuItem key={"profile"} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{"Profile"}</Typography>
</MenuItem>
<MenuItem key={"settings"} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{"Settings"}</Typography>
</MenuItem>
<MenuItem key={"logout"} onClick={handleLogout}>
<Typography textAlign="center">{"Logout"}</Typography>
</MenuItem>
))}
</Menu>
</Box>
</Toolbar>

View File

@ -12,6 +12,11 @@ import Register from "./routes/Auth/register";
import AuthRoot from "./routes/Auth/authRoot";
import { Provider } from "react-redux";
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(
document.getElementById("root") as HTMLElement
@ -28,6 +33,36 @@ const router = createBrowserRouter([
path: "/",
element: <Root />,
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",

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</>;
}