├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.js ├── actions │ ├── auth.js │ ├── locale.js │ └── users.js ├── api.js ├── components │ ├── forms │ │ ├── ForgotPasswordForm.js │ │ ├── LoginForm.js │ │ ├── ResetPasswordForm.js │ │ └── SignupForm.js │ ├── messages │ │ ├── ConfirmEmailMessage.js │ │ └── InlineError.js │ ├── navigation │ │ └── TopNavigation.js │ ├── pages │ │ ├── CharactersPage.js │ │ ├── ConfirmationPage.js │ │ ├── DashboardPage.js │ │ ├── ForgotPasswordPage.js │ │ ├── HomePage.js │ │ ├── LoginPage.js │ │ ├── NewCharacterPage.js │ │ ├── ResetPasswordPage.js │ │ └── SignupPage.js │ └── routes │ │ ├── GuestRoute.js │ │ └── UserRoute.js ├── history.js ├── index.js ├── messages.js ├── reducers │ ├── characters.js │ ├── formErrors.js │ ├── locale.js │ └── user.js ├── registerServiceWorker.js ├── rootReducer.js ├── rootSaga.js ├── sagas │ └── usersSagas.js ├── schemas.js ├── types.js └── utils │ └── setAuthorizationHeader.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | // install the following packages: 3 | // yarn add --dev eslint prettier eslint-config-airbnb@^15.0.1 eslint-config-prettier eslint-plugin-prettier eslint-plugin-react eslint-plugin-import eslint-plugin-jsx-a11y@^5.1.1 4 | 5 | "extends": ["airbnb", "prettier", "prettier/react"], 6 | "plugins": ["prettier"], 7 | "parser": "babel-eslint", 8 | "parserOptions": { 9 | "ecmaVersion": 2016, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "jsx": true 13 | } 14 | }, 15 | "env": { 16 | "es6": true, 17 | "browser": true, 18 | "node": true 19 | }, 20 | "rules": { 21 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 22 | "no-underscore-dangle": [1, { "allow": ["_id"] }] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # D&D Adventurers League Hub React Application 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alhub-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.17.1", 7 | "bootstrap": "4.0.0-beta.2", 8 | "glamor": "^2.20.40", 9 | "glamorous": "^4.11.0", 10 | "gravatar-url": "^2.0.0", 11 | "jwt-decode": "^2.2.0", 12 | "normalizr": "^3.2.4", 13 | "prop-types": "^15.6", 14 | "react": "^16.2.0", 15 | "react-animations": "^1.0.0", 16 | "react-dom": "^16.2.0", 17 | "react-intl": "^2.4.0", 18 | "react-loader": "^2.4.2", 19 | "react-redux": "^5.0.6", 20 | "react-router": "^4.2.0", 21 | "react-router-dom": "^4.2.2", 22 | "react-scripts": "1.0.17", 23 | "reactstrap": "^5.0.0-alpha.4", 24 | "redux": "^3.7.2", 25 | "redux-devtools-extension": "^2.13.2", 26 | "redux-saga": "^0.16.0", 27 | "redux-thunk": "^2.2.0", 28 | "reselect": "^3.0.1", 29 | "validator": "^9.1.2" 30 | }, 31 | "scripts": { 32 | "start": "PORT=3002 react-scripts start", 33 | "build": "react-scripts build", 34 | "test": "react-scripts test --env=jsdom", 35 | "eject": "react-scripts eject", 36 | "lint": "eslint src" 37 | }, 38 | "devDependencies": { 39 | "eslint": "^4.12.1", 40 | "eslint-config-airbnb": "^15.0.1", 41 | "eslint-config-prettier": "^2.3.0", 42 | "eslint-plugin-import": "^2.7.0", 43 | "eslint-plugin-jsx-a11y": "^5.1.1", 44 | "eslint-plugin-prettier": "^2.2.0", 45 | "eslint-plugin-react": "^7.3.0", 46 | "prettier": "^1.9.1" 47 | }, 48 | "proxy": "http://localhost:8082" 49 | } 50 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Remchi/alhub-react/4052b55c4dd264018309c04bf4778de5449f5467/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 24 | React App 25 | 26 | 27 | 28 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import { Route } from "react-router-dom"; 5 | import Loader from "react-loader"; 6 | import { IntlProvider } from "react-intl"; 7 | import HomePage from "./components/pages/HomePage"; 8 | import LoginPage from "./components/pages/LoginPage"; 9 | import DashboardPage from "./components/pages/DashboardPage"; 10 | import SignupPage from "./components/pages/SignupPage"; 11 | import ConfirmationPage from "./components/pages/ConfirmationPage"; 12 | import ForgotPasswordPage from "./components/pages/ForgotPasswordPage"; 13 | import ResetPasswordPage from "./components/pages/ResetPasswordPage"; 14 | import UserRoute from "./components/routes/UserRoute"; 15 | import GuestRoute from "./components/routes/GuestRoute"; 16 | import TopNavigation from "./components/navigation/TopNavigation"; 17 | import CharactersPage from "./components/pages/CharactersPage"; 18 | import NewCharacterPage from "./components/pages/NewCharacterPage"; 19 | import { fetchCurrentUserRequest } from "./actions/users"; 20 | import messages from "./messages"; 21 | 22 | class App extends React.Component { 23 | componentDidMount() { 24 | if (this.props.isAuthenticated) this.props.fetchCurrentUserRequest(); 25 | } 26 | 27 | render() { 28 | const { location, isAuthenticated, loaded, lang } = this.props; 29 | return ( 30 | 31 |
32 | 33 | {isAuthenticated && } 34 | 35 | 41 | 47 | 53 | 59 | 65 | 71 | 77 | 83 | 84 |
85 |
86 | ); 87 | } 88 | } 89 | 90 | App.propTypes = { 91 | location: PropTypes.shape({ 92 | pathname: PropTypes.string.isRequired 93 | }).isRequired, 94 | isAuthenticated: PropTypes.bool.isRequired, 95 | fetchCurrentUserRequest: PropTypes.func.isRequired, 96 | loaded: PropTypes.bool.isRequired, 97 | lang: PropTypes.string.isRequired 98 | }; 99 | 100 | function mapStateToProps(state) { 101 | return { 102 | isAuthenticated: !!state.user.email, 103 | loaded: state.user.loaded, 104 | lang: state.locale.lang 105 | }; 106 | } 107 | 108 | export default connect(mapStateToProps, { fetchCurrentUserRequest })(App); 109 | -------------------------------------------------------------------------------- /src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import { USER_LOGGED_IN, USER_LOGGED_OUT } from "../types"; 2 | import api from "../api"; 3 | import setAuthorizationHeader from "../utils/setAuthorizationHeader"; 4 | 5 | export const userLoggedIn = user => ({ 6 | type: USER_LOGGED_IN, 7 | user 8 | }); 9 | 10 | export const userLoggedOut = () => ({ 11 | type: USER_LOGGED_OUT 12 | }); 13 | 14 | export const login = credentials => dispatch => 15 | api.user.login(credentials).then(user => { 16 | localStorage.bookwormJWT = user.token; 17 | setAuthorizationHeader(user.token); 18 | dispatch(userLoggedIn({ ...user, loaded: true })); 19 | }); 20 | 21 | export const logout = () => dispatch => { 22 | localStorage.removeItem("bookwormJWT"); 23 | setAuthorizationHeader(); 24 | dispatch(userLoggedOut()); 25 | }; 26 | 27 | export const confirm = token => dispatch => 28 | api.user.confirm(token).then(user => { 29 | localStorage.bookwormJWT = user.token; 30 | dispatch(userLoggedIn(user)); 31 | }); 32 | 33 | export const resetPasswordRequest = ({ email }) => () => 34 | api.user.resetPasswordRequest(email); 35 | 36 | export const validateToken = token => () => api.user.validateToken(token); 37 | 38 | export const resetPassword = data => () => api.user.resetPassword(data); 39 | -------------------------------------------------------------------------------- /src/actions/locale.js: -------------------------------------------------------------------------------- 1 | import { LOCALE_SET } from "../types"; 2 | 3 | export const localeSet = lang => ({ 4 | type: LOCALE_SET, 5 | lang 6 | }); 7 | 8 | export const setLocale = lang => dispatch => { 9 | localStorage.alhubLang = lang; 10 | dispatch(localeSet(lang)); 11 | }; 12 | -------------------------------------------------------------------------------- /src/actions/users.js: -------------------------------------------------------------------------------- 1 | import { 2 | CREATE_USER_REQUEST, 3 | CREATE_USER_FAILURE, 4 | FETCH_CURRENT_USER_REQUEST, 5 | FETCH_CURRENT_USER_SUCCESS 6 | } from "../types"; 7 | 8 | export const createUserRequest = user => ({ 9 | type: CREATE_USER_REQUEST, 10 | user 11 | }); 12 | 13 | export const createUserFailure = errors => ({ 14 | type: CREATE_USER_FAILURE, 15 | errors 16 | }); 17 | 18 | export const fetchCurrentUserRequest = () => ({ 19 | type: FETCH_CURRENT_USER_REQUEST 20 | }); 21 | 22 | export const fetchCurrentUserSuccess = user => ({ 23 | type: FETCH_CURRENT_USER_SUCCESS, 24 | user 25 | }); 26 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export default { 4 | user: { 5 | login: credentials => 6 | axios.post("/api/auth", { credentials }).then(res => res.data.user), 7 | signup: user => 8 | axios.post("/api/users", { user }).then(res => res.data.user), 9 | confirm: token => 10 | axios 11 | .post("/api/auth/confirmation", { token }) 12 | .then(res => res.data.user), 13 | resetPasswordRequest: email => 14 | axios.post("/api/auth/reset_password_request", { email }), 15 | validateToken: token => axios.post("/api/auth/validate_token", { token }), 16 | resetPassword: data => axios.post("/api/auth/reset_password", { data }), 17 | fetchCurrentUser: () => 18 | axios.get("/api/users/current_user").then(res => res.data.user) 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/forms/ForgotPasswordForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import isEmail from "validator/lib/isEmail"; 5 | 6 | class ForgotPasswordForm extends React.Component { 7 | state = { 8 | data: { 9 | email: "" 10 | }, 11 | errors: {} 12 | }; 13 | 14 | onChange = e => 15 | this.setState({ 16 | ...this.state, 17 | data: { ...this.state.data, [e.target.name]: e.target.value } 18 | }); 19 | 20 | onSubmit = e => { 21 | e.preventDefault(); 22 | const errors = this.validate(this.state.data); 23 | this.setState({ errors }); 24 | if (Object.keys(errors).length === 0) { 25 | this.setState({ loading: true }); 26 | this.props 27 | .submit(this.state.data) 28 | .catch(err => 29 | this.setState({ errors: err.response.data.errors, loading: false }) 30 | ); 31 | } 32 | }; 33 | 34 | validate = data => { 35 | const errors = {}; 36 | if (!isEmail(data.email)) errors.email = "Invalid email"; 37 | return errors; 38 | }; 39 | 40 | render() { 41 | const { errors, data } = this.state; 42 | 43 | return ( 44 |
45 | {!!errors.global && ( 46 |
{errors.global}
47 | )} 48 | 49 |
50 | 51 | 61 |
{errors.email}
62 |
63 | 64 | 67 | 68 | 69 | Sign Up | 70 | Login 71 | 72 |
73 | ); 74 | } 75 | } 76 | 77 | ForgotPasswordForm.propTypes = { 78 | submit: PropTypes.func.isRequired 79 | }; 80 | 81 | export default ForgotPasswordForm; 82 | -------------------------------------------------------------------------------- /src/components/forms/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import Validator from "validator"; 5 | 6 | class LoginForm extends React.Component { 7 | state = { 8 | data: { 9 | email: "", 10 | password: "" 11 | }, 12 | errors: {} 13 | }; 14 | 15 | onChange = e => 16 | this.setState({ 17 | data: { ...this.state.data, [e.target.name]: e.target.value } 18 | }); 19 | 20 | onSubmit = e => { 21 | e.preventDefault(); 22 | const errors = this.validate(this.state.data); 23 | this.setState({ errors }); 24 | if (Object.keys(errors).length === 0) { 25 | this.setState({ loading: true }); 26 | this.props 27 | .submit(this.state.data) 28 | .catch(err => 29 | this.setState({ errors: err.response.data.errors, loading: false }) 30 | ); 31 | } 32 | }; 33 | 34 | validate = data => { 35 | const errors = {}; 36 | if (!Validator.isEmail(data.email)) errors.email = "Invalid email"; 37 | if (!data.password) errors.password = "Can't be blank"; 38 | return errors; 39 | }; 40 | 41 | render() { 42 | const { data, errors } = this.state; 43 | 44 | return ( 45 |
46 | {errors.global && ( 47 |
{errors.global}
48 | )} 49 | 50 |
51 | 52 | 62 |
{errors.email}
63 |
64 | 65 |
66 | 67 | 77 |
{errors.password}
78 |
79 | 80 | 83 | 84 | 85 | Sign up if you don't have an account
86 | Forgot Password? 87 |
88 |
89 | ); 90 | } 91 | } 92 | 93 | LoginForm.propTypes = { 94 | submit: PropTypes.func.isRequired 95 | }; 96 | 97 | export default LoginForm; 98 | -------------------------------------------------------------------------------- /src/components/forms/ResetPasswordForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | class ResetPasswordForm extends React.Component { 5 | state = { 6 | data: { 7 | token: this.props.token, 8 | password: "", 9 | passwordConfirmation: "" 10 | }, 11 | errors: {} 12 | }; 13 | 14 | onChange = e => 15 | this.setState({ 16 | ...this.state, 17 | data: { ...this.state.data, [e.target.name]: e.target.value } 18 | }); 19 | 20 | onSubmit = e => { 21 | e.preventDefault(); 22 | const errors = this.validate(this.state.data); 23 | this.setState({ errors }); 24 | if (Object.keys(errors).length === 0) { 25 | this.setState({ loading: true }); 26 | this.props 27 | .submit(this.state.data) 28 | .catch(err => 29 | this.setState({ errors: err.response.data.errors, loading: false }) 30 | ); 31 | } 32 | }; 33 | 34 | validate = data => { 35 | const errors = {}; 36 | if (!data.password) errors.password = "Can't be blank"; 37 | if (data.password !== data.passwordConfirmation) 38 | errors.password = "Passwords must match"; 39 | return errors; 40 | }; 41 | 42 | render() { 43 | const { errors, data } = this.state; 44 | 45 | return ( 46 |
47 |
48 | 49 | 59 |
{errors.password}
60 |
61 | 62 |
63 | 64 | 76 |
{errors.passwordConfirmation}
77 |
78 | 79 | 82 |
83 | ); 84 | } 85 | } 86 | 87 | ResetPasswordForm.propTypes = { 88 | submit: PropTypes.func.isRequired, 89 | token: PropTypes.string.isRequired 90 | }; 91 | 92 | export default ResetPasswordForm; 93 | -------------------------------------------------------------------------------- /src/components/forms/SignupForm.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import PropTypes from "prop-types"; 4 | import { Link } from "react-router-dom"; 5 | import isEmail from "validator/lib/isEmail"; 6 | import { createUserRequest } from "../../actions/users"; 7 | 8 | class SignupForm extends React.Component { 9 | state = { 10 | data: { 11 | email: "", 12 | username: "", 13 | password: "" 14 | }, 15 | errors: {} 16 | }; 17 | 18 | componentWillReceiveProps(nextProps) { 19 | this.setState({ errors: nextProps.serverErrors }); 20 | } 21 | 22 | onChange = e => 23 | this.setState({ 24 | data: { ...this.state.data, [e.target.name]: e.target.value } 25 | }); 26 | 27 | onSubmit = e => { 28 | e.preventDefault(); 29 | const errors = this.validate(this.state.data); 30 | this.setState({ errors }); 31 | if (Object.keys(errors).length === 0) { 32 | this.setState({ loading: true }); 33 | this.props.submit(this.state.data); 34 | } 35 | }; 36 | 37 | validate = data => { 38 | const errors = {}; 39 | 40 | if (!isEmail(data.email)) errors.email = "Invalid email"; 41 | if (!data.password) errors.password = "Can't be blank"; 42 | if (!data.username) errors.username = "Can't be blank"; 43 | 44 | return errors; 45 | }; 46 | 47 | render() { 48 | const { data, errors } = this.state; 49 | 50 | return ( 51 |
52 |
53 | 54 | 64 |
{errors.email}
65 |
66 | 67 |
68 | 69 | 79 |
{errors.username}
80 |
81 | 82 |
83 | 84 | 94 |
{errors.password}
95 |
96 | 97 | 100 | 101 | 102 | or LOGIN if you have an account 103 | 104 |
105 | ); 106 | } 107 | } 108 | 109 | function mapStateToProps(state) { 110 | return { 111 | serverErrors: state.formErrors.signup 112 | }; 113 | } 114 | 115 | SignupForm.propTypes = { 116 | submit: PropTypes.func.isRequired 117 | }; 118 | 119 | export default connect(mapStateToProps, { submit: createUserRequest })( 120 | SignupForm 121 | ); 122 | -------------------------------------------------------------------------------- /src/components/messages/ConfirmEmailMessage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ConfirmEmailMessage = () => ( 4 |
5 | Please, verify your email to unlock awesomeness 6 |
7 | ); 8 | 9 | export default ConfirmEmailMessage; 10 | -------------------------------------------------------------------------------- /src/components/messages/InlineError.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const InlineError = ({ text }) => ( 5 | {text} 6 | ); 7 | 8 | InlineError.propTypes = { 9 | text: PropTypes.string.isRequired 10 | }; 11 | 12 | export default InlineError; 13 | -------------------------------------------------------------------------------- /src/components/navigation/TopNavigation.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | Navbar, 5 | Nav, 6 | NavbarBrand, 7 | NavbarToggler, 8 | Collapse, 9 | NavItem, 10 | NavLink, 11 | UncontrolledDropdown, 12 | DropdownToggle, 13 | DropdownMenu, 14 | DropdownItem 15 | } from "reactstrap"; 16 | import { connect } from "react-redux"; 17 | import { NavLink as RouterNavLink } from "react-router-dom"; 18 | import gravatarUrl from "gravatar-url"; 19 | import { FormattedMessage } from "react-intl"; 20 | import * as actions from "../../actions/auth"; 21 | import { setLocale } from "../../actions/locale"; 22 | 23 | class TopNavigation extends React.Component { 24 | state = { 25 | isOpen: false 26 | }; 27 | 28 | toggle = () => this.setState({ isOpen: !this.state.isOpen }); 29 | 30 | render() { 31 | const { user, logout } = this.props; 32 | 33 | return ( 34 | 35 | 36 | ALHub 37 | 38 | 39 | 40 | 66 | 89 | 90 | 91 | ); 92 | } 93 | } 94 | 95 | TopNavigation.propTypes = { 96 | user: PropTypes.shape({ 97 | email: PropTypes.string.isRequired 98 | }).isRequired, 99 | logout: PropTypes.func.isRequired, 100 | setLocale: PropTypes.func.isRequired 101 | }; 102 | 103 | function mapStateToProps(state) { 104 | return { 105 | user: state.user 106 | }; 107 | } 108 | 109 | export default connect( 110 | mapStateToProps, 111 | { logout: actions.logout, setLocale }, 112 | null, 113 | { 114 | pure: false 115 | } 116 | )(TopNavigation); 117 | -------------------------------------------------------------------------------- /src/components/pages/CharactersPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "react-router-dom"; 4 | import { charactersSelector } from "../../reducers/characters"; 5 | import { connect } from "react-redux"; 6 | 7 | const CharactersPage = ({ characters }) => ( 8 |
9 | {characters.length === 0 && ( 10 |
11 |
12 | You have no characters yet. How about creating one? 13 |
14 | 15 | Create New Character 16 | 17 |
18 | )} 19 |
20 | ); 21 | 22 | CharactersPage.propTypes = { 23 | characters: PropTypes.arrayOf(PropTypes.object).isRequired 24 | }; 25 | 26 | function mapStateToProps(state) { 27 | return { 28 | characters: charactersSelector(state) 29 | }; 30 | } 31 | 32 | export default connect(mapStateToProps)(CharactersPage); 33 | -------------------------------------------------------------------------------- /src/components/pages/ConfirmationPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { Link } from "react-router-dom"; 5 | import { confirm } from "../../actions/auth"; 6 | 7 | class ConfirmationPage extends React.Component { 8 | state = { 9 | loading: true, 10 | success: false 11 | }; 12 | 13 | componentDidMount() { 14 | this.props 15 | .confirm(this.props.match.params.token) 16 | .then(() => this.setState({ loading: false, success: true })) 17 | .catch(() => this.setState({ loading: false, success: false })); 18 | } 19 | 20 | render() { 21 | const { loading, success } = this.state; 22 | 23 | return ( 24 |
25 | {loading && ( 26 |
Validating your account...
27 | )} 28 | 29 | {!loading && 30 | success && ( 31 |
32 | Thank you! Your account has been verified. Now you can go to your 33 | dashboard 34 |
35 | )} 36 | 37 | {!loading && 38 | !success && ( 39 |
40 | Ooops. Invalid token it seems. 41 |
42 | )} 43 |
44 | ); 45 | } 46 | } 47 | 48 | ConfirmationPage.propTypes = { 49 | confirm: PropTypes.func.isRequired, 50 | match: PropTypes.shape({ 51 | params: PropTypes.shape({ 52 | token: PropTypes.string.isRequired 53 | }).isRequired 54 | }).isRequired 55 | }; 56 | 57 | export default connect(null, { confirm })(ConfirmationPage); 58 | -------------------------------------------------------------------------------- /src/components/pages/DashboardPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import ConfirmEmailMessage from "../messages/ConfirmEmailMessage"; 5 | 6 | class DashboardPage extends React.Component { 7 | render() { 8 | const { isConfirmed } = this.props; 9 | return ( 10 |
11 | {!isConfirmed && } 12 |
13 | ); 14 | } 15 | } 16 | 17 | DashboardPage.propTypes = { 18 | isConfirmed: PropTypes.bool.isRequired 19 | }; 20 | 21 | function mapStateToProps(state) { 22 | return { 23 | isConfirmed: !!state.user.confirmed 24 | }; 25 | } 26 | 27 | export default connect(mapStateToProps)(DashboardPage); 28 | -------------------------------------------------------------------------------- /src/components/pages/ForgotPasswordPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import ForgotPasswordForm from "../forms/ForgotPasswordForm"; 5 | import { resetPasswordRequest } from "../../actions/auth"; 6 | 7 | class ForgotPasswordPage extends React.Component { 8 | state = { 9 | success: false 10 | }; 11 | 12 | submit = data => 13 | this.props 14 | .resetPasswordRequest(data) 15 | .then(() => this.setState({ success: true })); 16 | 17 | render() { 18 | return ( 19 |
20 |
21 |
22 |
23 |
24 |

Recover Password

25 |
26 | {this.state.success ? ( 27 |
Email has been sent.
28 | ) : ( 29 | 30 | )} 31 |
32 |
33 |
34 |
35 |
36 |
37 | ); 38 | } 39 | } 40 | 41 | ForgotPasswordPage.propTypes = { 42 | resetPasswordRequest: PropTypes.func.isRequired 43 | }; 44 | 45 | export default connect(null, { resetPasswordRequest })(ForgotPasswordPage); 46 | -------------------------------------------------------------------------------- /src/components/pages/HomePage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { Container, Row, Col } from "reactstrap"; 4 | 5 | const HomePage = () => ( 6 | 14 | 18 | 19 | Adventurers League Logo 24 | 25 | 32 |

40 | BECOME AN ADVENTURER! 41 |

42 |
43 |
44 | 52 | JOIN THE PARTY! 53 | 54 |
55 | 56 |
57 |
58 | ); 59 | 60 | export default HomePage; 61 | -------------------------------------------------------------------------------- /src/components/pages/LoginPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import LoginForm from "../forms/LoginForm"; 5 | import { login } from "../../actions/auth"; 6 | 7 | class LoginPage extends React.Component { 8 | submit = data => 9 | this.props.login(data).then(() => this.props.history.push("/dashboard")); 10 | 11 | render() { 12 | return ( 13 |
14 |
15 |
16 |
17 |
18 |

Welcome Back!

19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | ); 28 | } 29 | } 30 | 31 | LoginPage.propTypes = { 32 | history: PropTypes.shape({ 33 | push: PropTypes.func.isRequired 34 | }).isRequired, 35 | login: PropTypes.func.isRequired 36 | }; 37 | 38 | export default connect(null, { login })(LoginPage); 39 | -------------------------------------------------------------------------------- /src/components/pages/NewCharacterPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { fadeIn } from "react-animations"; 3 | import * as glamor from "glamor"; 4 | import glamorous from "glamorous"; 5 | 6 | const FadeInDiv = glamorous.div({ 7 | animation: `1s ${glamor.css.keyframes(fadeIn)}` 8 | }); 9 | 10 | class NewCharacterPage extends Component { 11 | state = { 12 | step: 1 13 | }; 14 | 15 | render() { 16 | return ( 17 |
18 | 65 | 66 |
67 | {this.state.step === 1 && ( 68 | 69 |

STEP 1

70 |
71 | )} 72 | {this.state.step === 2 && ( 73 | 74 |

STEP 2

75 |
76 | )} 77 | {this.state.step === 3 && ( 78 | 79 |

STEP 3

80 |
81 | )} 82 | {this.state.step === 4 && ( 83 | 84 |

STEP 4

85 |
86 | )} 87 | {this.state.step === 5 && ( 88 | 89 |

STEP 5

90 |
91 | )} 92 |
93 |
94 | ); 95 | } 96 | } 97 | 98 | export default NewCharacterPage; 99 | -------------------------------------------------------------------------------- /src/components/pages/ResetPasswordPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { Link } from "react-router-dom"; 5 | import ResetPasswordForm from "../forms/ResetPasswordForm"; 6 | import { validateToken, resetPassword } from "../../actions/auth"; 7 | 8 | class ResetPasswordPage extends React.Component { 9 | state = { 10 | loading: true, 11 | success: false 12 | }; 13 | 14 | componentDidMount() { 15 | this.props 16 | .validateToken(this.props.match.params.token) 17 | .then(() => this.setState({ loading: false, success: true })) 18 | .catch(() => this.setState({ loading: false, success: false })); 19 | } 20 | 21 | submit = data => 22 | this.props 23 | .resetPassword(data) 24 | .then(() => this.props.history.push("/login")); 25 | 26 | render() { 27 | const { loading, success } = this.state; 28 | const token = this.props.match.params.token; 29 | 30 | return ( 31 |
32 |
33 |
34 |
35 |
36 |

Set New Password

37 |
38 | {loading &&
Loading
} 39 | {!loading && 40 | success && ( 41 | 42 | )} 43 | {!loading && 44 | !success && ( 45 |
46 | Invalid Token. Try to{" "} 47 | recover password{" "} 48 | again. 49 |
50 | )} 51 |
52 |
53 |
54 |
55 |
56 |
57 | ); 58 | } 59 | } 60 | 61 | ResetPasswordPage.propTypes = { 62 | validateToken: PropTypes.func.isRequired, 63 | resetPassword: PropTypes.func.isRequired, 64 | match: PropTypes.shape({ 65 | params: PropTypes.shape({ 66 | token: PropTypes.string.isRequired 67 | }).isRequired 68 | }).isRequired, 69 | history: PropTypes.shape({ 70 | push: PropTypes.func.isRequired 71 | }).isRequired 72 | }; 73 | 74 | export default connect(null, { validateToken, resetPassword })( 75 | ResetPasswordPage 76 | ); 77 | -------------------------------------------------------------------------------- /src/components/pages/SignupPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SignupForm from "../forms/SignupForm"; 3 | 4 | const SignupPage = () => ( 5 |
6 |
7 |
8 |
9 |

Join the Club!

10 |
11 | 12 |
13 |
14 |
15 |
16 |
17 | ); 18 | 19 | export default SignupPage; 20 | -------------------------------------------------------------------------------- /src/components/routes/GuestRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { Route, Redirect } from "react-router-dom"; 5 | 6 | const UserRoute = ({ isAuthenticated, component: Component, ...rest }) => ( 7 | 10 | !isAuthenticated ? ( 11 | 12 | ) : ( 13 | 14 | )} 15 | /> 16 | ); 17 | 18 | UserRoute.propTypes = { 19 | component: PropTypes.func.isRequired, 20 | isAuthenticated: PropTypes.bool.isRequired 21 | }; 22 | 23 | function mapStateToProps(state) { 24 | return { 25 | isAuthenticated: !!state.user.token 26 | }; 27 | } 28 | 29 | export default connect(mapStateToProps)(UserRoute); 30 | -------------------------------------------------------------------------------- /src/components/routes/UserRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { connect } from "react-redux"; 4 | import { Route, Redirect } from "react-router-dom"; 5 | 6 | const UserRoute = ({ isAuthenticated, component: Component, ...rest }) => ( 7 | 10 | isAuthenticated ? : 11 | } 12 | /> 13 | ); 14 | 15 | UserRoute.propTypes = { 16 | component: PropTypes.func.isRequired, 17 | isAuthenticated: PropTypes.bool.isRequired 18 | }; 19 | 20 | function mapStateToProps(state) { 21 | return { 22 | isAuthenticated: !!state.user.email 23 | }; 24 | } 25 | 26 | export default connect(mapStateToProps)(UserRoute); 27 | -------------------------------------------------------------------------------- /src/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from "history"; 2 | 3 | export default createBrowserHistory(); 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Router, Route } from "react-router-dom"; 4 | import "bootstrap/dist/css/bootstrap.css"; 5 | import { createStore, applyMiddleware } from "redux"; 6 | import { Provider } from "react-redux"; 7 | import thunk from "redux-thunk"; 8 | import { addLocaleData } from "react-intl"; 9 | import en from "react-intl/locale-data/en"; 10 | import ru from "react-intl/locale-data/ru"; 11 | import { composeWithDevTools } from "redux-devtools-extension"; 12 | import createSagaMiddleware from "redux-saga"; 13 | import App from "./App"; 14 | import registerServiceWorker from "./registerServiceWorker"; 15 | import rootReducer from "./rootReducer"; 16 | import { 17 | fetchCurrentUserSuccess, 18 | fetchCurrentUserRequest 19 | } from "./actions/users"; 20 | import { localeSet } from "./actions/locale"; 21 | import setAuthorizationHeader from "./utils/setAuthorizationHeader"; 22 | import rootSaga from "./rootSaga"; 23 | import history from "./history"; 24 | 25 | addLocaleData(en); 26 | addLocaleData(ru); 27 | 28 | const sagaMiddleware = createSagaMiddleware(); 29 | const store = createStore( 30 | rootReducer, 31 | composeWithDevTools(applyMiddleware(sagaMiddleware, thunk)) 32 | ); 33 | sagaMiddleware.run(rootSaga); 34 | 35 | if (localStorage.bookwormJWT) { 36 | setAuthorizationHeader(localStorage.bookwormJWT); 37 | store.dispatch(fetchCurrentUserRequest()); 38 | } else { 39 | store.dispatch(fetchCurrentUserSuccess({})); 40 | } 41 | 42 | if (localStorage.alhubLang) { 43 | store.dispatch(localeSet(localStorage.alhubLang)); 44 | } 45 | 46 | ReactDOM.render( 47 | 48 | 49 | 50 | 51 | , 52 | document.getElementById("root") 53 | ); 54 | registerServiceWorker(); 55 | -------------------------------------------------------------------------------- /src/messages.js: -------------------------------------------------------------------------------- 1 | export default { 2 | en: { 3 | "nav.dashboard": "Dashboard" 4 | }, 5 | ru: { 6 | "nav.dashboard": "Панель Управления" 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /src/reducers/characters.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from "reselect"; 2 | 3 | export default function characters(state = {}, action = {}) { 4 | switch (action.type) { 5 | default: 6 | return state; 7 | } 8 | } 9 | 10 | export const charactersHashSelector = state => state.characters; 11 | 12 | export const charactersSelector = createSelector(charactersHashSelector, hash => 13 | Object.values(hash) 14 | ); 15 | -------------------------------------------------------------------------------- /src/reducers/formErrors.js: -------------------------------------------------------------------------------- 1 | import { CREATE_USER_FAILURE, CREATE_USER_REQUEST } from "../types"; 2 | 3 | export default function formErrors(state = {}, action = {}) { 4 | switch (action.type) { 5 | case CREATE_USER_REQUEST: 6 | return { ...state, signup: {} }; 7 | case CREATE_USER_FAILURE: 8 | return { ...state, signup: action.errors }; 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/reducers/locale.js: -------------------------------------------------------------------------------- 1 | import { LOCALE_SET } from "../types"; 2 | 3 | export default function locale(state = { lang: "en" }, action = {}) { 4 | switch (action.type) { 5 | case LOCALE_SET: 6 | return { lang: action.lang }; 7 | default: 8 | return state; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { 2 | USER_LOGGED_IN, 3 | USER_LOGGED_OUT, 4 | FETCH_CURRENT_USER_SUCCESS 5 | } from "../types"; 6 | 7 | export default function user(state = { loaded: false }, action = {}) { 8 | switch (action.type) { 9 | case USER_LOGGED_IN: 10 | return { ...action.user, loaded: true }; 11 | case FETCH_CURRENT_USER_SUCCESS: 12 | return { ...state, ...action.user, loaded: true }; 13 | case USER_LOGGED_OUT: 14 | return { loaded: true }; 15 | default: 16 | return state; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // In production, we register a service worker to serve assets from local cache. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on the "N+1" visit to a page, since previously 7 | // cached resources are updated in the background. 8 | 9 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 10 | // This link also includes instructions on opting out of this behavior. 11 | 12 | const isLocalhost = Boolean( 13 | window.location.hostname === "localhost" || 14 | // [::1] is the IPv6 localhost address. 15 | window.location.hostname === "[::1]" || 16 | // 127.0.0.1/8 is considered localhost for IPv4. 17 | window.location.hostname.match( 18 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 19 | ) 20 | ); 21 | 22 | export default function register() { 23 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 24 | // The URL constructor is available in all browsers that support SW. 25 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 26 | if (publicUrl.origin !== window.location.origin) { 27 | // Our service worker won't work if PUBLIC_URL is on a different origin 28 | // from what our page is served on. This might happen if a CDN is used to 29 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 30 | return; 31 | } 32 | 33 | window.addEventListener("load", () => { 34 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 35 | 36 | if (!isLocalhost) { 37 | // Is not local host. Just register service worker 38 | registerValidSW(swUrl); 39 | } else { 40 | // This is running on localhost. Lets check if a service worker still exists or not. 41 | checkValidServiceWorker(swUrl); 42 | } 43 | }); 44 | } 45 | } 46 | 47 | function registerValidSW(swUrl) { 48 | navigator.serviceWorker 49 | .register(swUrl) 50 | .then(registration => { 51 | registration.onupdatefound = () => { 52 | const installingWorker = registration.installing; 53 | installingWorker.onstatechange = () => { 54 | if (installingWorker.state === "installed") { 55 | if (navigator.serviceWorker.controller) { 56 | // At this point, the old content will have been purged and 57 | // the fresh content will have been added to the cache. 58 | // It's the perfect time to display a "New content is 59 | // available; please refresh." message in your web app. 60 | console.log("New content is available; please refresh."); 61 | } else { 62 | // At this point, everything has been precached. 63 | // It's the perfect time to display a 64 | // "Content is cached for offline use." message. 65 | console.log("Content is cached for offline use."); 66 | } 67 | } 68 | }; 69 | }; 70 | }) 71 | .catch(error => { 72 | console.error("Error during service worker registration:", error); 73 | }); 74 | } 75 | 76 | function checkValidServiceWorker(swUrl) { 77 | // Check if the service worker can be found. If it can't reload the page. 78 | fetch(swUrl) 79 | .then(response => { 80 | // Ensure service worker exists, and that we really are getting a JS file. 81 | if ( 82 | response.status === 404 || 83 | response.headers.get("content-type").indexOf("javascript") === -1 84 | ) { 85 | // No service worker found. Probably a different app. Reload the page. 86 | navigator.serviceWorker.ready.then(registration => { 87 | registration.unregister().then(() => { 88 | window.location.reload(); 89 | }); 90 | }); 91 | } else { 92 | // Service worker found. Proceed as normal. 93 | registerValidSW(swUrl); 94 | } 95 | }) 96 | .catch(() => { 97 | console.log( 98 | "No internet connection found. App is running in offline mode." 99 | ); 100 | }); 101 | } 102 | 103 | export function unregister() { 104 | if ("serviceWorker" in navigator) { 105 | navigator.serviceWorker.ready.then(registration => { 106 | registration.unregister(); 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import user from "./reducers/user"; 4 | import characters from "./reducers/characters"; 5 | import locale from "./reducers/locale"; 6 | import formErrors from "./reducers/formErrors"; 7 | 8 | export default combineReducers({ 9 | user, 10 | characters, 11 | locale, 12 | formErrors 13 | }); 14 | -------------------------------------------------------------------------------- /src/rootSaga.js: -------------------------------------------------------------------------------- 1 | import { takeLatest } from "redux-saga/effects"; 2 | import { CREATE_USER_REQUEST, FETCH_CURRENT_USER_REQUEST } from "./types"; 3 | import { createUserSaga, fetchUserSaga } from "./sagas/usersSagas"; 4 | 5 | export default function* rootSaga() { 6 | yield takeLatest(CREATE_USER_REQUEST, createUserSaga); 7 | yield takeLatest(FETCH_CURRENT_USER_REQUEST, fetchUserSaga); 8 | } 9 | -------------------------------------------------------------------------------- /src/sagas/usersSagas.js: -------------------------------------------------------------------------------- 1 | import { call, put } from "redux-saga/effects"; 2 | import { userLoggedIn } from "../actions/auth"; 3 | import { createUserFailure } from "../actions/users"; 4 | import api from "../api"; 5 | import history from "../history"; 6 | 7 | export function* createUserSaga(action) { 8 | try { 9 | const user = yield call(api.user.signup, action.user); 10 | localStorage.bookwormJWT = user.token; 11 | yield put(userLoggedIn(user)); 12 | history.push("/dashboard"); 13 | } catch (err) { 14 | yield put(createUserFailure(err.response.data.errors)); 15 | } 16 | } 17 | 18 | export function* fetchUserSaga() { 19 | const user = yield call(api.user.fetchCurrentUser); 20 | yield put(userLoggedIn(user)); 21 | } 22 | -------------------------------------------------------------------------------- /src/schemas.js: -------------------------------------------------------------------------------- 1 | import { schema } from "normalizr"; 2 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | export const USER_LOGGED_IN = "USER_LOGGED_IN"; 2 | export const USER_LOGGED_OUT = "USER_LOGGED_OUT"; 3 | export const LOCALE_SET = "LOCALE_SET"; 4 | 5 | export const CREATE_USER_REQUEST = "CREATE_USER_REQUEST"; 6 | export const CREATE_USER_FAILURE = "CREATE_USER_FAILURE"; 7 | export const FETCH_CURRENT_USER_REQUEST = "FETCH_CURRENT_USER_REQUEST"; 8 | export const FETCH_CURRENT_USER_SUCCESS = "FETCH_CURRENT_USER_SUCCESS"; 9 | -------------------------------------------------------------------------------- /src/utils/setAuthorizationHeader.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export default (token = null) => { 4 | if (token) { 5 | axios.defaults.headers.common.authorization = `Bearer ${token}`; 6 | } else { 7 | delete axios.defaults.headers.common.authorization; 8 | } 9 | }; 10 | --------------------------------------------------------------------------------