🏗️ Views and routes boilerplate
Signed-off-by: Pau Costa <mico@micodev.es>pull/2/head
parent
31ba7bead1
commit
591dae9567
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 userInfo = useSelector(selectUserInfo);
|
||||
const [anchorElUser, setAnchorElUser] = React.useState<null | HTMLElement>(
|
||||
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>
|
||||
))}
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export default function NewPost() {
|
||||
return <>New Post</>;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default function Notification() {
|
||||
return <>Notification</>;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default function Post() {
|
||||
return <>Post</>;
|
||||
}
|
||||
|
|
@ -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>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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</>;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export default function Search() {
|
||||
return <>Search</>;
|
||||
}
|
||||
Loading…
Reference in New Issue