diff --git a/src/App.js b/src/App.js index 4442af9..4b8246f 100644 --- a/src/App.js +++ b/src/App.js @@ -1,17 +1,29 @@ // src/App.js -import React from "react"; +import React, { useEffect } from "react"; +import { useDispatch } from "react-redux"; import "./App.css"; import { Routes, Route } from "react-router-dom"; import Homepage from "./pages/Homepage"; import PostPage from "./pages/PostPage"; +import LoginPage from "./pages/Login"; +import Toolbar from "./components/Toolbar"; +import { bootstrapLoginState } from "./store/auth/actions"; export default function App() { + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(bootstrapLoginState()); + }, []); + return (
+ {/* more pages to be added here later */} } /> } /> + } />
); diff --git a/src/components/Toolbar.js b/src/components/Toolbar.js new file mode 100644 index 0000000..81a45ec --- /dev/null +++ b/src/components/Toolbar.js @@ -0,0 +1,32 @@ +import { Link } from "react-router-dom"; +import { useSelector, useDispatch } from "react-redux"; +import { getUserProfile } from "../store/auth/selectors"; +import { logout } from "../store/auth/slice"; +import "./styles.css"; + +const Toolbar = () => { + const user = useSelector(getUserProfile); + const dispatch = useDispatch(); + + return ( +
+ +

Codaisseur Coders Network!

+ +
+ {user ? ( + <> +

Welcome {user.name}

+ + + ) : ( + + + + )} +
+
+ ); +}; + +export default Toolbar; diff --git a/src/components/styles.css b/src/components/styles.css new file mode 100644 index 0000000..fc1d25c --- /dev/null +++ b/src/components/styles.css @@ -0,0 +1,21 @@ +.toolbar { + display: flex; + justify-content: space-between; + padding-right: 30px; + padding-left: 30px; + background-color: lightsalmon; +} + +.button-container { + display: flex; + align-items: center; +} + +.welcome-text { + margin-right: 30px; +} + +.logo-link { + text-decoration: none; + color: black; +} diff --git a/src/pages/Login.js b/src/pages/Login.js new file mode 100644 index 0000000..f231b7f --- /dev/null +++ b/src/pages/Login.js @@ -0,0 +1,52 @@ +// src/pages/LoginPage.js +import React, { useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { login } from "../store/auth/actions"; +import { getAuthLoading } from "../store/auth/selectors"; + +export default function LoginPage() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const loading = useSelector(getAuthLoading); + + function handleSubmit(event) { + event.preventDefault(); + dispatch(login(email, password, navigate)); + } + + return ( +
+

Login

+
+

+ +

+

+ +

+

+ +

+
+ {loading && "Loading..."} +
+ ); +} diff --git a/src/store/auth/actions.js b/src/store/auth/actions.js new file mode 100644 index 0000000..6e2f583 --- /dev/null +++ b/src/store/auth/actions.js @@ -0,0 +1,46 @@ +// src/store/auth/actions.js +import axios from "axios"; +import { API_URL } from "../../config"; +import { startLoading, userLoggedIn } from "./slice"; +// A thunk creator +export const login = (email, password, navigate) => { + return async function thunk(dispatch, getState) { + try { + dispatch(startLoading()); + + const response = await axios.post(`${API_URL}/login`, { + email, + password, + }); + + const { jwt } = response.data; + + const profileResponse = await axios.get(`${API_URL}/me`, { + headers: { authorization: `Bearer ${jwt}` }, + }); + + localStorage.setItem("token", jwt); + + dispatch(userLoggedIn({ accessToken: jwt, user: profileResponse.data })); + navigate("/"); + } catch (e) { + console.log("Error at login", e.message); + } + }; +}; + +export const bootstrapLoginState = () => async (dispatch, getState) => { + try { + const token = localStorage.getItem("token"); + + if (!token) return; + + const response = await axios.get(`${API_URL}/me`, { + headers: { authorization: `Bearer ${token}` }, + }); + + dispatch(userLoggedIn({ accessToken: token, user: response.data })); + } catch (e) { + console.log(e.message); + } +}; diff --git a/src/store/auth/selectors.js b/src/store/auth/selectors.js new file mode 100644 index 0000000..635915a --- /dev/null +++ b/src/store/auth/selectors.js @@ -0,0 +1,2 @@ +export const getUserProfile = (reduxState) => reduxState.auth.me; +export const getAuthLoading = (reduxState) => reduxState.auth.loading; diff --git a/src/store/auth/slice.js b/src/store/auth/slice.js new file mode 100644 index 0000000..83ba55a --- /dev/null +++ b/src/store/auth/slice.js @@ -0,0 +1,33 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = { + me: null, // the logged-in user + accessToken: null, + loading: false, +}; + +export const authSlice = createSlice({ + name: "auth", + initialState, + reducers: { + startLoading: (state) => { + state.loading = true; + }, + userLoggedIn: (state, action) => { + state.me = action.payload.user; + state.accessToken = action.payload.accessToken; + state.loading = false; + }, + logout: (state) => { + localStorage.removeItem("token"); + // when we want to update the whole state + // we can return a new object instead of updating + // each key one by one + return initialState; + }, + }, +}); + +export const { startLoading, userLoggedIn, logout } = authSlice.actions; + +export default authSlice.reducer; diff --git a/src/store/index.js b/src/store/index.js index 98aa5e1..5b27baa 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2,11 +2,13 @@ import { configureStore } from "@reduxjs/toolkit"; import feedReducer from "./feed/slice"; import postPageReducer from "./postPage/slice"; +import authReducer from "./auth/slice"; const store = configureStore({ reducer: { feed: feedReducer, postPage: postPageReducer, + auth: authReducer, }, });