├── src ├── stylesheets │ ├── overides.scss │ ├── variables.scss │ ├── main.scss │ └── mixins.scss ├── pages │ ├── home-page │ │ ├── home-page.scss │ │ └── HomePage.js │ ├── posts-page │ │ ├── posts-page.scss │ │ └── PostsPage.js │ ├── signup-page │ │ ├── signup-page.scss │ │ └── SignUpPage.js │ └── signin-page │ │ ├── signin-page.scss │ │ └── SigninPage.js ├── components │ ├── signin-form │ │ ├── signin-form.scss │ │ └── SigninForm.js │ ├── signup-form │ │ ├── signup-form.scss │ │ └── SignupForm.js │ ├── navbar │ │ ├── navbar.scss │ │ └── Navbar.js │ └── svg │ │ └── Logo.js ├── reducers │ ├── index.js │ ├── posts-reducer.js │ └── auth-reducer.js ├── actions │ ├── posts-actions │ │ ├── types.js │ │ ├── service.js │ │ └── actions.js │ └── auth-actions │ │ ├── types.js │ │ ├── service.js │ │ └── actions.js ├── stores │ └── store-dev.js ├── config │ └── axios-instance.js ├── index.js └── App.js ├── .env.example ├── img ├── 1.png ├── 2.png └── 3.png ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── .gitignore ├── package.json └── README.md /src/stylesheets/overides.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/stylesheets/variables.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL= 2 | -------------------------------------------------------------------------------- /img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halberio/halber-react-kit/HEAD/img/1.png -------------------------------------------------------------------------------- /img/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halberio/halber-react-kit/HEAD/img/2.png -------------------------------------------------------------------------------- /img/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halberio/halber-react-kit/HEAD/img/3.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halberio/halber-react-kit/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halberio/halber-react-kit/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/halberio/halber-react-kit/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/pages/home-page/home-page.scss: -------------------------------------------------------------------------------- 1 | @import "../../stylesheets/variables"; 2 | @import "../../stylesheets/mixins"; 3 | 4 | .home-page { 5 | .title { 6 | margin-top: 100px; 7 | text-align: center; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/signin-form/signin-form.scss: -------------------------------------------------------------------------------- 1 | .signin-form { 2 | width: 100%; 3 | max-width: 400px; 4 | display: block; 5 | margin: auto; 6 | .submit-button { 7 | margin: auto; 8 | display: block; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/signup-form/signup-form.scss: -------------------------------------------------------------------------------- 1 | .signup-form { 2 | width: 100%; 3 | max-width: 400px; 4 | display: block; 5 | margin: auto; 6 | .submit-button { 7 | margin: auto; 8 | display: block; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/stylesheets/main.scss: -------------------------------------------------------------------------------- 1 | @import "mixins"; 2 | @import "variables"; 3 | @import "overides"; 4 | 5 | html { 6 | font-size: 12px; 7 | } 8 | body { 9 | padding: 0; 10 | margin: 0; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import authReducer from "./auth-reducer"; 3 | import postsReducer from "./posts-reducer"; 4 | 5 | const rootReducer = combineReducers({ 6 | authReducer, 7 | postsReducer 8 | }); 9 | 10 | export default rootReducer; 11 | -------------------------------------------------------------------------------- /src/actions/posts-actions/types.js: -------------------------------------------------------------------------------- 1 | /* 2 | @ 3 | This file contains the types constants 4 | @ 5 | */ 6 | 7 | export const FETCH_POSTS_REQUEST = "FETCH_POSTS_REQUEST "; 8 | export const FETCH_POSTS_SUCCESS = "FETCH_POSTS_SUCCESS "; 9 | export const FETCH_POSTS_FAILURE = "FETCH_POSTS_FAILURE "; 10 | -------------------------------------------------------------------------------- /src/pages/posts-page/posts-page.scss: -------------------------------------------------------------------------------- 1 | @import "../../stylesheets/variables"; 2 | @import "../../stylesheets/mixins"; 3 | 4 | .posts-page { 5 | @include flexbox(); 6 | @include flex-wrap(wrap); 7 | @include justify-content(center); 8 | 9 | .ant-card { 10 | margin: 10px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/signup-page/signup-page.scss: -------------------------------------------------------------------------------- 1 | @import "../../stylesheets/mixins"; 2 | .signup-page { 3 | .title { 4 | display: block; 5 | margin: 100px 0; 6 | text-align: center; 7 | } 8 | .content { 9 | @include flexbox(); 10 | @include justify-content(center); 11 | @include align-items(center); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/actions/posts-actions/service.js: -------------------------------------------------------------------------------- 1 | /* 2 | @ 3 | This file contains the requests ( services ) 4 | @ 5 | */ 6 | 7 | import axios from "axios"; 8 | 9 | function fetchPostsRequest() { 10 | return axios.get("https://jsonplaceholder.typicode.com/posts"); 11 | } 12 | 13 | const PostsServices = { 14 | fetchPostsRequest 15 | }; 16 | 17 | export default PostsServices; 18 | -------------------------------------------------------------------------------- /src/pages/signin-page/signin-page.scss: -------------------------------------------------------------------------------- 1 | @import "../../stylesheets/mixins"; 2 | @import "../../stylesheets/variables"; 3 | 4 | .signin-page { 5 | .title { 6 | display: block; 7 | margin: 100px 0; 8 | text-align: center; 9 | } 10 | .content { 11 | @include flexbox(); 12 | @include justify-content(center); 13 | @include align-items(center); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/home-page/HomePage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./home-page.scss"; 3 | import { Typography } from "antd"; 4 | 5 | const HomePage = () => { 6 | return ( 7 |
8 | 9 | Developed by Halber 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default HomePage; 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | 27 | .idea/ 28 | /.idea 29 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/reducers/posts-reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_POSTS_FAILURE, 3 | FETCH_POSTS_REQUEST, 4 | FETCH_POSTS_SUCCESS 5 | } from "../actions/posts-actions/types"; 6 | 7 | const intialState = { 8 | posts: [] 9 | }; 10 | 11 | const postsReducer = (state = intialState, action) => { 12 | switch (action.type) { 13 | case FETCH_POSTS_REQUEST: 14 | return state; 15 | case FETCH_POSTS_SUCCESS: 16 | return { ...state, posts: action.payload }; 17 | 18 | case FETCH_POSTS_FAILURE: 19 | return state; 20 | 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | export default postsReducer; 27 | -------------------------------------------------------------------------------- /src/stores/store-dev.js: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import { applyMiddleware } from "redux"; 3 | import { createLogger } from "redux-logger"; 4 | import reduxThunk from "redux-thunk"; 5 | import rootReducer from "../reducers/index"; 6 | 7 | const logger = createLogger({ 8 | collapsed: true, 9 | duration: true, 10 | colors: { 11 | title: () => "#0294B5", 12 | prevState: () => "#7286E9", 13 | action: () => "#FF534D", 14 | nextState: () => "#1DB954", 15 | error: () => "#FF534D" 16 | } 17 | }); 18 | const store = createStore(rootReducer, applyMiddleware(reduxThunk, logger)); 19 | 20 | export default store; 21 | -------------------------------------------------------------------------------- /src/pages/signup-page/SignUpPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import WrappedSignupForm from "../../components/signup-form/SignupForm"; 4 | import { Typography } from "antd"; 5 | import { signup } from "../../actions/auth-actions/actions"; 6 | import "./signup-page.scss"; 7 | 8 | const SignUpPage = props => { 9 | return ( 10 |
11 | Sign up page 12 |
13 | 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default connect( 20 | null, 21 | { signup } 22 | )(SignUpPage); 23 | -------------------------------------------------------------------------------- /src/pages/signin-page/SigninPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import WrappedSigninForm from "../../components/signin-form/SigninForm"; 4 | import { Typography } from "antd"; 5 | import { signin } from "../../actions/auth-actions/actions"; 6 | 7 | import "./signin-page.scss"; 8 | 9 | const SigninPage = props => { 10 | return ( 11 |
12 | Sign in page 13 |
14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default connect( 21 | null, 22 | { signin } 23 | )(SigninPage); 24 | -------------------------------------------------------------------------------- /src/actions/posts-actions/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | @ 3 | This file contains the actions creators 4 | @ 5 | */ 6 | 7 | import { 8 | FETCH_POSTS_REQUEST, 9 | FETCH_POSTS_FAILURE, 10 | FETCH_POSTS_SUCCESS 11 | } from "./types"; 12 | 13 | import PostsServices from "./service"; 14 | 15 | export function fetchPosts() { 16 | return async dispatch => { 17 | dispatch({ 18 | type: FETCH_POSTS_REQUEST 19 | }); 20 | try { 21 | const response = await PostsServices.fetchPostsRequest(); 22 | 23 | await dispatch({ 24 | type: FETCH_POSTS_SUCCESS, 25 | payload: response.data 26 | }); 27 | } catch (e) { 28 | dispatch({ 29 | type: FETCH_POSTS_FAILURE 30 | }); 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/actions/auth-actions/types.js: -------------------------------------------------------------------------------- 1 | /* 2 | @ 3 | This file contains the types constants 4 | @ 5 | */ 6 | 7 | export const SIGNIN_REQUEST = "SIGNIN_REQUEST "; 8 | export const SIGNIN_FAILURE = "SIGNIN_FAILURE "; 9 | export const SIGNIN_SUCCESS = "SIGNIN_SUCCESS "; 10 | 11 | export const SIGNUP_REQUEST = "SIGNUP_REQUEST "; 12 | export const SIGNUP_ERROR = "SIGNUP_ERROR "; 13 | export const SIGNUP_SUCCESS = "SIGNUP_SUCCESS "; 14 | 15 | export const GET_AUTH_REQUEST = "GET_AUTH_REQUEST "; 16 | export const GET_AUTH_SUCCESS = "GET_AUTH_SUCCESS "; 17 | export const GET_AUTH_FAILURE = "GET_AUTH_FAILURE "; 18 | 19 | export const LOGOUT_REQUEST = "LOGOUT_REQUEST "; 20 | export const LOGOUT_FAILURE = "LOGOUT_FAILURE "; 21 | export const LOGOUT_SUCCESS = "LOGOUT_SUCCESS "; 22 | 23 | export const DISCONNECT_THE_USER = "DISCONNECT_THE_USER "; 24 | export const CONNECT_THE_USER = "CONNECT_THE_USER "; 25 | -------------------------------------------------------------------------------- /src/pages/posts-page/PostsPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { connect } from "react-redux"; 3 | import { fetchPosts } from "../../actions/posts-actions/actions"; 4 | import { Card } from "antd"; 5 | 6 | import "./posts-page.scss"; 7 | 8 | const PostsPage = props => { 9 | useEffect(() => { 10 | props.fetchPosts(); 11 | }, []); 12 | return ( 13 |
14 | {props.posts.map((ele, index) => { 15 | return ( 16 | 17 |

{ele.body}

18 |
19 | ); 20 | })} 21 |
22 | ); 23 | }; 24 | 25 | const mapStateToProps = reduxStore => { 26 | return { 27 | posts: reduxStore.postsReducer.posts 28 | }; 29 | }; 30 | 31 | export default connect( 32 | mapStateToProps, 33 | { fetchPosts } 34 | )(PostsPage); 35 | -------------------------------------------------------------------------------- /src/actions/auth-actions/service.js: -------------------------------------------------------------------------------- 1 | /* 2 | @ 3 | This file contains the requests ( services ) 4 | @ 5 | */ 6 | 7 | import axiosInstance from "../../config/axios-instance"; 8 | 9 | function logoutRequest() { 10 | return axiosInstance({ 11 | method: "get", 12 | url: "auth/logout", 13 | data: null 14 | }); 15 | } 16 | 17 | function signinRequest(body) { 18 | return axiosInstance({ 19 | method: "post", 20 | url: "auth/signin", 21 | data: body 22 | }); 23 | } 24 | 25 | function signupRequest(body) { 26 | return axiosInstance({ 27 | method: "post", 28 | url: "auth/signup", 29 | data: body 30 | }); 31 | } 32 | 33 | function getAuthUserRequest() { 34 | return axiosInstance({ 35 | method: "get", 36 | url: "auth/user" 37 | }); 38 | } 39 | 40 | const AuthServices = { 41 | signinRequest, 42 | signupRequest, 43 | logoutRequest, 44 | getAuthUserRequest 45 | }; 46 | 47 | export default AuthServices; 48 | -------------------------------------------------------------------------------- /src/components/navbar/navbar.scss: -------------------------------------------------------------------------------- 1 | @import "../../stylesheets/variables"; 2 | @import "../../stylesheets/mixins"; 3 | 4 | .navbar { 5 | @include flexbox(); 6 | @include align-items(center); 7 | .first-element { 8 | margin-left: 40px; 9 | } 10 | .navbar-brand, 11 | .navbar-avatar { 12 | @include flexbox(); 13 | @include align-items(center); 14 | position: relative; 15 | &.ant-menu-item-selected, 16 | &.ant-menu-item-active { 17 | color: transparent !important; 18 | border-bottom: 0 !important; 19 | } 20 | } 21 | .navbar-brand { 22 | .brand-link { 23 | position: relative; 24 | svg { 25 | position: absolute; 26 | top: 0; 27 | bottom: 0; 28 | margin: auto; 29 | height: 30px; 30 | width: 30px; 31 | } 32 | } 33 | } 34 | .navbar-right { 35 | margin-left: auto; 36 | } 37 | .avatar { 38 | i { 39 | margin: 0; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "halber-react-kit", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "antd": "^3.23.4", 7 | "axios": "^0.19.0", 8 | "history": "^4.10.1", 9 | "node-sass": "^4.12.0", 10 | "react": "^16.9.0", 11 | "react-dom": "^16.9.0", 12 | "react-redux": "^7.1.1", 13 | "react-router-dom": "^5.0.1", 14 | "react-scripts": "3.1.2", 15 | "redux": "^4.0.4", 16 | "redux-thunk": "^2.3.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "devDependencies": { 40 | "husky": "^3.0.5", 41 | "prettier": "1.18.2", 42 | "pretty-quick": "^1.11.1", 43 | "redux-logger": "^3.0.6" 44 | }, 45 | "husky": { 46 | "hooks": { 47 | "pre-commit": "pretty-quick --staged" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/config/axios-instance.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import store from "../stores/store-dev"; 3 | import { logout } from "../actions/auth-actions/actions"; 4 | 5 | import { history } from "../index"; 6 | 7 | const token = localStorage.getItem("halber_token"); 8 | 9 | const axiosInstance = axios.create({ 10 | baseURL: process.env.REACT_APP_API_URL, 11 | headers: { 12 | Accept: "application/json", 13 | "Content-Type": "application/json", 14 | Authorization: `Bearer ${token}` 15 | } 16 | }); 17 | 18 | axiosInstance.interceptors.request.use( 19 | function(config) { 20 | // Do something before request is sent 21 | return config; 22 | }, 23 | function(error) { 24 | // Do something with request error 25 | return Promise.reject(error); 26 | } 27 | ); 28 | 29 | // Add a response interceptor 30 | axiosInstance.interceptors.response.use( 31 | function(response) { 32 | // Do something with response data 33 | return response; 34 | }, 35 | function(error) { 36 | switch (error.response.status) { 37 | case 401: 38 | // unauthorized -> token is invalid or expired 39 | // User must reconnect! 40 | store.dispatch(logout()); 41 | history.push("/login"); 42 | break; 43 | default: 44 | break; 45 | } 46 | // Do something with response error 47 | return Promise.reject(error); 48 | } 49 | ); 50 | 51 | export default axiosInstance; 52 | -------------------------------------------------------------------------------- /src/components/signin-form/SigninForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Form, Icon, Input, Button } from "antd"; 3 | 4 | import "./signin-form.scss"; 5 | 6 | const SigninForm = props => { 7 | const { getFieldDecorator } = props.form; 8 | 9 | const handleSubmit = e => { 10 | e.preventDefault(); 11 | props.form.validateFields((err, data) => { 12 | if (!err) { 13 | props.signin(data); 14 | } 15 | }); 16 | }; 17 | 18 | return ( 19 |
20 | 21 | {getFieldDecorator("email", { 22 | rules: [{ required: true, message: "Please input your email!" }] 23 | })( 24 | } 26 | placeholder="Email" 27 | /> 28 | )} 29 | 30 | 31 | {getFieldDecorator("password", { 32 | rules: [{ required: true, message: "Please input your Password!" }] 33 | })( 34 | } 36 | type="password" 37 | placeholder="Password" 38 | /> 39 | )} 40 | 41 | 42 | 45 | 46 |
47 | ); 48 | }; 49 | 50 | const WrappedSigninForm = Form.create({ name: "normal_login" })(SigninForm); 51 | 52 | export default WrappedSigninForm; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## **Halber-react-kit** 2 | 3 | a ready to use starter kit with authentication based on token. 4 | 5 | **NB:** 6 | Do not forget to rename the .env.example file to .env and put there your REST API url 7 | 8 | ## **Packages installed** and ready to use: 9 | 10 | - Ant design 11 | - Redux 12 | - Axios ( with config & interceptors ) 13 | - SCSS ( with a lot of MIXINS ) 14 | 15 | ## **Project structure:** 16 | 17 | - actions 18 | - components 19 | - config 20 | - pages 21 | - reducers 22 | - stores 23 | - stylesheets 24 | - tools 25 | - App.js 26 | - index.js 27 | 28 | ## **Useful informations** 29 | 30 | - In the config folder you find a file named api.js, here you need to add your Restful API url 31 | - Also in config folder you can find the used axios instance for all requests so you can do things before or after sending & getting requests or responses 32 | - I recommend to keep this structure of the project so you can work with a lot of conventions 33 | - My Restful API is made with Laravel passport 34 | - We are also trying to keep this project up to date by adding new features and using latest versions of packages 35 | - For any other information or question don't hesitate to contact us. 36 | 37 | # 38 | 39 | Email : **hello@halber.io** 40 | 41 | Website : **https://halber.io** 42 | 43 | # 44 | 45 | **Sign in page:** 46 | ![alt text](https://raw.githubusercontent.com/halberio/halber-react-kit/master/img/1.png) 47 | 48 | # 49 | 50 | **Sign up page:** 51 | ![alt text](https://raw.githubusercontent.com/halberio/halber-react-kit/master/img/2.png) 52 | 53 | # 54 | 55 | **Articles page:** 56 | ![alt text](https://raw.githubusercontent.com/halberio/halber-react-kit/master/img/3.png) 57 | 58 | # 59 | -------------------------------------------------------------------------------- /src/components/svg/Logo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Logo = () => { 4 | return ( 5 | 11 | 16 | 21 | 28 | 29 | 34 | 41 | 42 | 47 | 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | export default Logo; 61 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/navbar/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Avatar, Dropdown, Menu } from "antd"; 3 | import { NavLink } from "react-router-dom"; 4 | import "./navbar.scss"; 5 | import Logo from "../svg/Logo"; 6 | 7 | const Navbar = props => { 8 | return ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {props.isLoggedIn ? ( 18 | 19 | Posts 20 | 21 | ) : null} 22 | 23 | {!props.isLoggedIn ? ( 24 | 25 | signin 26 | 27 | ) : null} 28 | 29 | {!props.isLoggedIn ? ( 30 | 31 | signup 32 | 33 | ) : null} 34 | 35 | {props.isLoggedIn ? ( 36 | 37 | 40 | } 41 | trigger={["click"]} 42 | > 43 | 44 | 45 | 46 | ) : null} 47 | 48 | 49 | ); 50 | }; 51 | export default Navbar; 52 | 53 | const AvatarDropdown = props => { 54 | return ( 55 | 56 | Hello {props.user.name} 57 | props.logout()}> 58 | Logout 59 | 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Router } from "react-router-dom"; 4 | import { Provider, connect } from "react-redux"; 5 | import { createBrowserHistory } from "history"; 6 | import axiosInstance from "./config/axios-instance"; 7 | import store from "./stores/store-dev"; 8 | 9 | import App from "./App"; 10 | 11 | import "antd/dist/antd.min.css"; 12 | import "./stylesheets/main.scss"; 13 | 14 | import { connectTheUser, getAuthUser } from "./actions/auth-actions/actions"; 15 | 16 | export const history = createBrowserHistory(); 17 | 18 | const token = localStorage.getItem("halber_token"); 19 | 20 | if (token) { 21 | // if token exists in local storage! 22 | store.dispatch(connectTheUser(token)); 23 | } 24 | 25 | store.subscribe(() => { 26 | const reduxSubs = store.getState(); 27 | if (reduxSubs.authReducer.token) { 28 | axiosInstance.defaults.headers.common[ 29 | "Authorization" 30 | ] = `Bearer ${reduxSubs.authReducer.token}`; 31 | axiosInstance.defaults.headers[ 32 | "Authorization" 33 | ] = `Bearer ${reduxSubs.authReducer.token}`; 34 | } 35 | }); 36 | 37 | const WrappedApp = props => { 38 | useEffect(() => { 39 | if (token) { 40 | // We need to check if the token are valid or not by getting the auth user 41 | props.store.dispatch(getAuthUser()); 42 | } 43 | }, []); 44 | 45 | return ( 46 | <> 47 | {/*if token is available we try to get the user once each time the app gets reloaded, so we don't need to 48 | fetch the auth user everytime we need him,*/} 49 | 50 | {token && props.isLoadingUser ?

Loading...

: props.children} 51 | 52 | ); 53 | }; 54 | 55 | const mapStateToProps = reduxStore => { 56 | return { 57 | isLoadingUser: reduxStore.authReducer.isLoadingUser 58 | }; 59 | }; 60 | 61 | const ConnectedWrappedApp = connect(mapStateToProps)(WrappedApp); 62 | 63 | ReactDOM.render( 64 | 65 | 66 | 67 | 68 | 69 | 70 | , 71 | document.getElementById("root") 72 | ); 73 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { Route, Redirect, Switch, withRouter } from "react-router-dom"; 4 | import HomePage from "./pages/home-page/HomePage"; 5 | import Navbar from "./components/navbar/Navbar"; 6 | import SigninPage from "./pages/signin-page/SigninPage"; 7 | 8 | import SignUpPage from "./pages/signup-page/SignUpPage"; 9 | import { logout } from "./actions/auth-actions/actions"; 10 | import PostsPage from "./pages/posts-page/PostsPage"; 11 | 12 | const App = props => { 13 | return ( 14 |
15 | 20 | 21 | 22 | 27 | 32 | 37 | /> 38 | 39 |
40 | ); 41 | }; 42 | function AuthRoute({ component: Component, authenticated, ...rest }) { 43 | return ( 44 | 48 | authenticated ? ( 49 | 50 | ) : ( 51 | 54 | ) 55 | } 56 | /> 57 | ); 58 | } 59 | 60 | function GuestRoute({ component: Component, authenticated, ...rest }) { 61 | return ( 62 | 66 | !authenticated ? : 67 | } 68 | /> 69 | ); 70 | } 71 | 72 | const mapStateToProps = reduxStore => { 73 | return { 74 | isLoggedIn: reduxStore.authReducer.isLoggedIn, 75 | user: reduxStore.authReducer.user, 76 | isLoadingUser: reduxStore.authReducer.isLoadingUser 77 | }; 78 | }; 79 | 80 | export default withRouter( 81 | connect( 82 | mapStateToProps, 83 | { logout } 84 | )(App) 85 | ); 86 | -------------------------------------------------------------------------------- /src/components/signup-form/SignupForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Form, Icon, Input, Button } from "antd"; 3 | 4 | import "./signup-form.scss"; 5 | 6 | const SignupForm = props => { 7 | const { getFieldDecorator } = props.form; 8 | const handleSubmit = e => { 9 | e.preventDefault(); 10 | props.form.validateFields((err, data) => { 11 | if (!err) { 12 | props.signup(data); 13 | } 14 | }); 15 | }; 16 | return ( 17 |
18 | 19 | {getFieldDecorator("name", { 20 | rules: [{ required: true, message: "Please input your name!" }] 21 | })( 22 | } 24 | placeholder="Name" 25 | /> 26 | )} 27 | 28 | 29 | 30 | {getFieldDecorator("email", { 31 | rules: [{ required: true, message: "Please input your email!" }] 32 | })( 33 | } 35 | placeholder="Email" 36 | /> 37 | )} 38 | 39 | 40 | {getFieldDecorator("password", { 41 | rules: [{ required: true, message: "Please input your Password!" }] 42 | })( 43 | } 45 | type="password" 46 | placeholder="Password" 47 | /> 48 | )} 49 | 50 | 51 | {getFieldDecorator("password_confirmation", { 52 | rules: [ 53 | { 54 | required: true, 55 | message: "Please input your Password confirmation!" 56 | } 57 | ] 58 | })( 59 | } 61 | type="password" 62 | placeholder="Password confirmation" 63 | /> 64 | )} 65 | 66 | 67 | 70 | 71 |
72 | ); 73 | }; 74 | 75 | const WrappedSignupForm = Form.create({ name: "normal_login" })(SignupForm); 76 | 77 | export default WrappedSignupForm; 78 | -------------------------------------------------------------------------------- /src/actions/auth-actions/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | @ 3 | This file contains the actions creators 4 | @ 5 | */ 6 | 7 | import { 8 | SIGNIN_FAILURE, 9 | SIGNIN_REQUEST, 10 | SIGNIN_SUCCESS, 11 | SIGNUP_REQUEST, 12 | SIGNUP_SUCCESS, 13 | SIGNUP_ERROR, 14 | GET_AUTH_SUCCESS, 15 | GET_AUTH_FAILURE, 16 | GET_AUTH_REQUEST, 17 | LOGOUT_REQUEST, 18 | CONNECT_THE_USER, 19 | LOGOUT_SUCCESS, 20 | LOGOUT_FAILURE 21 | } from "./types"; 22 | 23 | import AuthServices from "./service"; 24 | 25 | export function getAuthUser() { 26 | return async dispatch => { 27 | await dispatch({ 28 | type: GET_AUTH_REQUEST 29 | }); 30 | try { 31 | const response = await AuthServices.getAuthUserRequest(); 32 | await dispatch({ 33 | type: GET_AUTH_SUCCESS, 34 | payload: { 35 | user: response.data, 36 | isLoggedIn: true 37 | } 38 | }); 39 | } catch (e) { 40 | dispatch({ 41 | type: GET_AUTH_FAILURE 42 | }); 43 | } 44 | }; 45 | } 46 | 47 | export function signin(values) { 48 | return async dispatch => { 49 | dispatch({ type: SIGNIN_REQUEST }); 50 | try { 51 | const response = await AuthServices.signinRequest(values); 52 | dispatch({ type: SIGNIN_SUCCESS, payload: response.data }); 53 | localStorage.setItem("halber_token", response.data.access_token); 54 | } catch (e) { 55 | dispatch({ type: SIGNIN_FAILURE }); 56 | } 57 | }; 58 | } 59 | 60 | export function signup(body) { 61 | return async dispatch => { 62 | dispatch({ type: SIGNUP_REQUEST }); 63 | try { 64 | const response = await AuthServices.signupRequest(body); 65 | dispatch({ type: SIGNUP_SUCCESS, payload: response.data }); 66 | localStorage.setItem("halber_token", response.data.access_token); 67 | } catch (e) { 68 | dispatch({ type: SIGNUP_ERROR }); 69 | } 70 | }; 71 | } 72 | 73 | export function logout() { 74 | return async dispatch => { 75 | dispatch({ type: LOGOUT_REQUEST }); 76 | try { 77 | await AuthServices.logoutRequest(); 78 | localStorage.removeItem("halber_token"); 79 | dispatch({ type: LOGOUT_SUCCESS }); 80 | } catch (e) { 81 | dispatch({ type: LOGOUT_FAILURE }); 82 | } 83 | }; 84 | } 85 | 86 | export function connectTheUser(token) { 87 | return async dispatch => { 88 | localStorage.setItem("halber_token", token); 89 | dispatch({ 90 | type: CONNECT_THE_USER, 91 | payload: { 92 | token: token 93 | } 94 | }); 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /src/reducers/auth-reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SIGNIN_FAILURE, 3 | SIGNIN_REQUEST, 4 | SIGNIN_SUCCESS, 5 | SIGNUP_REQUEST, 6 | SIGNUP_SUCCESS, 7 | SIGNUP_ERROR, 8 | LOGOUT_REQUEST, 9 | DISCONNECT_THE_USER, 10 | CONNECT_THE_USER, 11 | GET_AUTH_REQUEST, 12 | GET_AUTH_SUCCESS, 13 | GET_AUTH_FAILURE, 14 | LOGOUT_FAILURE, 15 | LOGOUT_SUCCESS 16 | } from "../actions/auth-actions/types"; 17 | 18 | const intialState = { 19 | user: null, 20 | isLoggedIn: false, 21 | isLoadingUser: true, 22 | token: null 23 | }; 24 | 25 | const authReducer = (state = intialState, action) => { 26 | switch (action.type) { 27 | case GET_AUTH_REQUEST: 28 | return { 29 | ...state, 30 | isLoadingUser: true 31 | }; 32 | case GET_AUTH_SUCCESS: 33 | return { 34 | ...state, 35 | user: action.payload.user, 36 | isLoadingUser: false 37 | }; 38 | 39 | case GET_AUTH_FAILURE: 40 | return { 41 | ...state, 42 | isLoadingUser: false, 43 | isLoggedIn: false 44 | }; 45 | 46 | // Sign in 47 | case SIGNIN_REQUEST: 48 | return state; 49 | 50 | case SIGNIN_SUCCESS: 51 | return { 52 | ...state, 53 | user: action.payload.user, 54 | isLoggedIn: true, 55 | token: action.payload.access_token 56 | }; 57 | case SIGNIN_FAILURE: 58 | return state; 59 | 60 | //Sign up 61 | case SIGNUP_REQUEST: 62 | return state; 63 | case SIGNUP_SUCCESS: 64 | return { 65 | ...state, 66 | user: action.payload.user, 67 | isLoggedIn: true, 68 | token: action.payload.access_token 69 | }; 70 | case SIGNUP_ERROR: 71 | return state; 72 | 73 | // Logout 74 | 75 | case LOGOUT_REQUEST: 76 | return state; 77 | 78 | case LOGOUT_SUCCESS: 79 | return { 80 | ...state, 81 | isLoggedIn: false, 82 | token: null 83 | }; 84 | case LOGOUT_FAILURE: 85 | return state; 86 | 87 | // Connect & disconnect user ( no interaction with the server ) 88 | case DISCONNECT_THE_USER: 89 | return { 90 | ...state, 91 | user: null, 92 | isLoggedIn: false, 93 | token: null 94 | }; 95 | 96 | case CONNECT_THE_USER: 97 | return { 98 | ...state, 99 | isLoggedIn: true, 100 | token: action.payload.token // getting token from local storage 101 | }; 102 | default: 103 | return state; 104 | } 105 | }; 106 | 107 | export default authReducer; 108 | -------------------------------------------------------------------------------- /src/stylesheets/mixins.scss: -------------------------------------------------------------------------------- 1 | // -------------------------------------------------- 2 | // Flexbox SASS mixins 3 | // The spec: http://www.w3.org/TR/css3-flexbox 4 | // -------------------------------------------------- 5 | 6 | // Flexbox display 7 | @mixin flexbox() { 8 | display: -webkit-box; 9 | display: -moz-box; 10 | display: -ms-flexbox; 11 | display: -webkit-flex; 12 | display: flex; 13 | } 14 | @mixin filter-gradient($direction, $color-stops...) { 15 | // Direction has been omitted and happens to be a color-stop 16 | @if is-direction($direction) == false { 17 | $color-stops: $direction, $color-stops; 18 | $direction: 180deg; 19 | } 20 | 21 | background: nth(nth($color-stops, 1), 1); 22 | background: -webkit-linear-gradient( 23 | legacy-direction($direction), 24 | $color-stops 25 | ); 26 | background: linear-gradient($direction, $color-stops); 27 | } 28 | // The 'flex' shorthand 29 | // - applies to: flex items 30 | // , initial, auto, or none 31 | @mixin flex($values) { 32 | -webkit-box-flex: $values; 33 | -moz-box-flex: $values; 34 | -webkit-flex: $values; 35 | -ms-flex: $values; 36 | flex: $values; 37 | } 38 | 39 | // Flex Flow Direction 40 | // - applies to: flex containers 41 | // row | row-reverse | column | column-reverse 42 | @mixin flex-direction($direction) { 43 | -webkit-flex-direction: $direction; 44 | -moz-flex-direction: $direction; 45 | -ms-flex-direction: $direction; 46 | flex-direction: $direction; 47 | } 48 | 49 | // Flex Line Wrapping 50 | // - applies to: flex containers 51 | // nowrap | wrap | wrap-reverse 52 | @mixin flex-wrap($wrap) { 53 | -webkit-flex-wrap: $wrap; 54 | -moz-flex-wrap: $wrap; 55 | -ms-flex-wrap: $wrap; 56 | flex-wrap: $wrap; 57 | } 58 | 59 | // Flex Direction and Wrap 60 | // - applies to: flex containers 61 | // || 62 | @mixin flex-flow($flow) { 63 | -webkit-flex-flow: $flow; 64 | -moz-flex-flow: $flow; 65 | -ms-flex-flow: $flow; 66 | flex-flow: $flow; 67 | } 68 | 69 | // Display Order 70 | // - applies to: flex items 71 | // 72 | @mixin order($val) { 73 | -webkit-box-ordinal-group: $val; 74 | -moz-box-ordinal-group: $val; 75 | -ms-flex-order: $val; 76 | -webkit-order: $val; 77 | order: $val; 78 | } 79 | 80 | // Flex grow factor 81 | // - applies to: flex items 82 | // 83 | @mixin flex-grow($grow) { 84 | -webkit-flex-grow: $grow; 85 | -moz-flex-grow: $grow; 86 | -ms-flex-grow: $grow; 87 | flex-grow: $grow; 88 | } 89 | 90 | // Flex shrink 91 | // - applies to: flex item shrink factor 92 | // 93 | @mixin flex-shrink($shrink) { 94 | -webkit-flex-shrink: $shrink; 95 | -moz-flex-shrink: $shrink; 96 | -ms-flex-shrink: $shrink; 97 | flex-shrink: $shrink; 98 | } 99 | 100 | // Flex basis 101 | // - the initial main size of the flex item 102 | // - applies to: flex itemsnitial main size of the flex item 103 | // 104 | @mixin flex-basis($width) { 105 | -webkit-flex-basis: $width; 106 | -moz-flex-basis: $width; 107 | -ms-flex-basis: $width; 108 | flex-basis: $width; 109 | } 110 | 111 | // Axis Alignment 112 | // - applies to: flex containers 113 | // flex-start | flex-end | center | space-between | space-around 114 | @mixin justify-content($justify) { 115 | -webkit-justify-content: $justify; 116 | -moz-justify-content: $justify; 117 | -ms-justify-content: $justify; 118 | justify-content: $justify; 119 | -ms-flex-pack: $justify; 120 | } 121 | 122 | // Packing Flex Lines 123 | // - applies to: multi-line flex containers 124 | // flex-start | flex-end | center | space-between | space-around | stretch 125 | @mixin align-content($align) { 126 | -webkit-align-content: $align; 127 | -moz-align-content: $align; 128 | -ms-align-content: $align; 129 | align-content: $align; 130 | } 131 | 132 | // Cross-axis Alignment 133 | // - applies to: flex containers 134 | // flex-start | flex-end | center | baseline | stretch 135 | @mixin align-items($align) { 136 | -webkit-align-items: $align; 137 | -moz-align-items: $align; 138 | -ms-align-items: $align; 139 | -ms-flex-align: $align; 140 | align-items: $align; 141 | } 142 | 143 | // Cross-axis Alignment 144 | // - applies to: flex items 145 | // auto | flex-start | flex-end | center | baseline | stretch 146 | @mixin align-self($align) { 147 | -webkit-align-self: $align; 148 | -moz-align-self: $align; 149 | -ms-align-self: $align; 150 | align-self: $align; 151 | } 152 | 153 | @mixin box-shadow($left, $top, $radius, $color) { 154 | box-shadow: $left $top $radius $color; 155 | -webkit-box-shadow: $left $top $radius $color; 156 | -moz-box-shadow: $left $top $radius $color; 157 | } 158 | 159 | @mixin transition($property, $duration, $easing: linear) { 160 | transition: $property $duration $easing; 161 | -webkit-transition: $property $duration $easing; 162 | -moz-transition: $property $duration $easing; 163 | } 164 | 165 | @mixin border-radius($radius) { 166 | border-radius: $radius; 167 | -webkit-border-radius: $radius; 168 | -moz-border-radius: $radius; 169 | } 170 | 171 | @mixin border-radii($topleft, $topright, $bottomright, $bottomleft) { 172 | border-top-left-radius: $topleft; 173 | border-top-right-radius: $topright; 174 | border-bottom-right-radius: $bottomright; 175 | border-bottom-left-radius: $bottomleft; 176 | -webkit-border-top-left-radius: $topleft; 177 | -webkit-border-top-right-radius: $topright; 178 | -webkit-border-bottom-right-radius: $bottomright; 179 | -webkit-border-bottom-left-radius: $bottomleft; 180 | -moz-border-radius-topleft: $topleft; 181 | -moz-border-radius-topright: $topright; 182 | -moz-border-radius-bottomright: $bottomright; 183 | -moz-border-radius-bottomleft: $bottomleft; 184 | } 185 | 186 | @mixin gradient($color1, $color2) { 187 | background-color: $color1; 188 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr=#{$color1}, endColorstr=#{$color2}); 189 | background-image: -moz-linear-gradient(center top, $color1, $color2); 190 | background-image: -webkit-gradient( 191 | linear, 192 | 0% 0%, 193 | 0% 100%, 194 | from($color1), 195 | to($color2) 196 | ); 197 | } 198 | 199 | // Browser Prefixes 200 | @mixin transform($transforms) { 201 | -webkit-transform: $transforms; 202 | -moz-transform: $transforms; 203 | -ms-transform: $transforms; 204 | transform: $transforms; 205 | } 206 | 207 | // Rotate 208 | @mixin rotate($deg) { 209 | @include transform(rotate(#{$deg}deg)); 210 | } 211 | 212 | // Scale 213 | @mixin scale($scale) { 214 | @include transform(scale($scale)); 215 | } 216 | 217 | // Translate 218 | @mixin translate($x, $y) { 219 | @include transform(translate($x, $y)); 220 | } 221 | 222 | // Skew 223 | @mixin skew($x, $y) { 224 | @include transform(skew(#{$x}deg, #{$y}deg)); 225 | } 226 | 227 | // Transform Origin 228 | @mixin transform-origin($origin) { 229 | -webkit-transform-origin: $origin; 230 | -moz-transform-origin: $origin; 231 | -ms-transform-origin: $origin; 232 | transform-origin: $origin; 233 | } 234 | --------------------------------------------------------------------------------