├── .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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------