* install redux and set authSlice

* connect navBar to the sotre

* create User type

* install react-redux-firebase

* bind to firebase

* connect App to firebase auth; display splash screen while loading auth state

* install firestore

* install firestore

* enable interactive form

* signup page functional

* fix navbar bug

* extract useForm hook

* extract Alert component

* sign in page functional

* commenting

* log out function

* add private route
This commit is contained in:
Ruidy 2020-05-14 14:18:22 +02:00 committed by GitHub
parent 7333e3474b
commit 07dd7c5624
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 395 additions and 618 deletions

View file

@ -24,6 +24,7 @@
"moment": "^2.25.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-google-button": "^0.7.1",
"react-redux": "^7.2.0",
"react-redux-firebase": "^3.4.0",
"react-router-dom": "^5.2.0",

15
src/components/Alert.tsx Normal file
View file

@ -0,0 +1,15 @@
import React, {FC} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons';
interface IProps {
text: string;
}
const Alert: FC<IProps> = ({text}) => (
<div className="alert alert-danger">
<FontAwesomeIcon icon={faExclamationTriangle} /> {text}
</div>
);
export default Alert;

View file

@ -3,14 +3,17 @@ import React, {FC} from 'react';
import {Link} from 'react-router-dom';
import * as ROUTES from '../constants/routes';
//Redux
import {compose} from '@reduxjs/toolkit';
import {connect} from 'react-redux';
import {withFirebase, WithFirebaseProps} from 'react-redux-firebase';
import {selectProfile} from '../store/firebase';
// import {selectAuthState} from '../store/auth';
// Style
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCode, faSignOutAlt, faUser} from '@fortawesome/free-solid-svg-icons';
// Typing
import User from '../models/User';
interface IProps {
interface IProps extends WithFirebaseProps<User> {
isEmpty: boolean;
isLoaded: boolean;
}
@ -18,7 +21,7 @@ interface IProps {
/**
* Main Navbar serves navigation routes.
*/
const NavBar: FC<IProps> = ({isEmpty, isLoaded}) => {
const NavBar: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
const publicLinks = (
<ul data-testid="publicLinks">
<li>
@ -58,7 +61,11 @@ const NavBar: FC<IProps> = ({isEmpty, isLoaded}) => {
</Link>
</li>
<li>
<Link to={ROUTES.SIGN_IN} data-testid="logoutLink">
<Link
to={ROUTES.SIGN_IN}
data-testid="logoutLink"
onClick={() => firebase.logout()}
>
<FontAwesomeIcon icon={faSignOutAlt} />
<span className="hide-sm"> Log out</span>
</Link>
@ -67,7 +74,7 @@ const NavBar: FC<IProps> = ({isEmpty, isLoaded}) => {
);
/** Display appropriated links after loading given authenticated prop */
const RenderLinks = !isLoaded && !isEmpty ? privateLinks : publicLinks;
const RenderLinks = isLoaded && !isEmpty ? privateLinks : publicLinks;
return (
<nav className="navbar bg-dark">
@ -82,5 +89,6 @@ const NavBar: FC<IProps> = ({isEmpty, isLoaded}) => {
};
/** connect HOC subscribes to the store */
export default connect(selectProfile)(NavBar);
//NavBar;
const enhance = compose<FC>(connect(selectProfile), withFirebase);
export default enhance(NavBar);

28
src/hooks/index.ts Normal file
View file

@ -0,0 +1,28 @@
import {useState, ChangeEvent} from 'react';
/**
* provide onChange handler and reset function
* T is the initFormData object type
*
* @param initFormData initial state of the form
* @returns formData object,
* @returns handleChange function to pass to input tag
* @returns resetForm function to revert to initFormData
* */
const useForm = <T,>(initFormData: T) => {
const [formData, setFormData] = useState<T>(initFormData);
/** update each input state value onChange */
const handleChange = (e: ChangeEvent<HTMLInputElement>): void =>
setFormData({
...formData,
[e.target.name]: e.target.value,
});
/** clean form after successful submition */
const resetForm = () => setFormData(initFormData);
return {formData, handleChange, resetForm};
};
export default useForm;

View file

@ -1,8 +1,20 @@
interface User {
name: string;
displayName: string;
email: string;
picture: string;
avatarUrl: string;
createdAt: Date;
}
/** User constructor */
export const newUser = (
displayName: string,
email: string,
avatarUrl: string = '',
): User => ({
displayName,
email,
avatarUrl,
createdAt: new Date(),
});
export default User;

View file

@ -1,29 +1,112 @@
import React, {FC} from 'react';
import React, {FC, useState} from 'react';
// Redux
import {compose} from '@reduxjs/toolkit';
import {connect} from 'react-redux';
import {WithFirebaseProps, withFirebase} from 'react-redux-firebase';
import {selectProfile} from '../store/firebase';
// Routing
import {Link, Redirect} from 'react-router-dom';
import * as ROUTES from '../constants/routes';
// Style
import GoogleButton from 'react-google-button';
import Header from '../components/Header';
import {Link} from 'react-router-dom';
import Alert from '../components/Alert';
// Typing
import User from '../models/User';
// Form
import useForm from '../hooks';
interface InitFormData {
email: string;
password: string;
}
interface IProps extends WithFirebaseProps<User> {
isEmpty: boolean;
isLoaded: boolean;
}
/**
* Sign in form
*/
const SignIn: FC = () => (
<section className="container">
<div className="alert alert-danger">Invalid credentials</div>
<Header title="Sign In" lead="Sign into your account" />
<form action="dashboard.html" className="form">
<div className="form-group">
<input type="email" placeholder="Email Address" />
</div>
<div className="form-group">
<input type="password" placeholder="Password" minLength={6} />
</div>
const SignIn: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
const [error, setError] = useState<any>(null);
<input type="submit" value="Login" className="btn btn-primary" />
</form>
<p className="my-1">
Don't have an account? <Link to={ROUTES.SIGN_UP}>Sign up</Link>
</p>
</section>
);
// handle form data
const initFormData: InitFormData = {
email: '',
password: '',
};
const {formData, handleChange, resetForm} = useForm<InitFormData>(
initFormData,
);
const {email, password} = formData;
export default SignIn;
// prevent submitting invalid forms
const isDisabled: boolean = email === '' || password === '';
/** create user with password */
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
firebase
.login({email, password})
.then(() => resetForm())
.catch(err => setError(err));
};
const loginWithGoogle = () =>
firebase.login({provider: 'google', type: 'popup'});
// redirect to dashboard if connected
if (isLoaded && !isEmpty) {
return <Redirect to={ROUTES.DASHBOARD} />;
}
return (
<section className="container">
{error && <Alert text={error?.message} />}
<Header title="Sign In" lead="Sign into your account" />
<GoogleButton type="light" className="my-1" onClick={loginWithGoogle} />
<form onSubmit={handleSubmit} className="form">
<div className="form-group">
<input
name="email"
value={email}
onChange={handleChange}
placeholder="Email Address"
type="email"
required
autoFocus
/>
</div>
<div className="form-group">
<input
name="password"
value={password}
onChange={handleChange}
placeholder="Password"
type="password"
minLength={6}
required
/>
</div>
<input
type="submit"
value="Login"
className="btn btn-primary"
disabled={isDisabled}
/>
</form>
<p className="my-1">
Don't have an account? <Link to={ROUTES.SIGN_UP}>Sign up</Link>
</p>
</section>
);
};
/** subscribe to store and firebase */
const enhance = compose<FC<IProps>>(connect(selectProfile), withFirebase);
export default enhance(SignIn);

View file

@ -1,35 +1,145 @@
import React, {FC} from 'react';
import {Link} from 'react-router-dom';
import Header from '../components/Header';
import React, {FC, useState} from 'react';
// Routing
import {Link, Redirect} from 'react-router-dom';
import * as ROUTES from '../constants/routes';
/**
* Sign up form
*/
const SignUp: FC = () => (
<section className="container">
<Header title="Sign Up" lead="Create your account" />
<form action="dashboard.html" className="form">
<div className="form-group">
<input type="text" placeholder="Name" required />
</div>
<div className="form-group">
<input type="email" placeholder="Email Address" />
<small className="form-text">
This site uses Gravatar, so use a Gravatar email.
</small>
</div>
<div className="form-group">
<input type="password" placeholder="Password" minLength={6} />
</div>
<div className="form-group">
<input type="password" placeholder="Confirm Password" minLength={6} />
</div>
<input type="submit" value="Register" className="btn btn-primary" />
</form>
<p className="my-1">
Already have an account? <Link to={ROUTES.SIGN_IN}>Sign in</Link>
</p>
</section>
);
// Redux
import {compose} from 'redux';
import {connect} from 'react-redux';
import {withFirebase, WithFirebaseProps} from 'react-redux-firebase';
import {selectProfile} from '../store/firebase';
import User, {newUser} from '../models/User';
// Style
import GoogleButton from 'react-google-button';
import Alert from '../components/Alert';
import Header from '../components/Header';
// Form
import useForm from '../hooks';
export default SignUp;
// extends withFirebaseProps type to ad profile info
interface IProps extends WithFirebaseProps<User> {
isEmpty: boolean;
isLoaded: boolean;
}
interface InitFormData {
name: string;
email: string;
password: string;
password2: string;
}
/**
* Sign up form recieves firebase from withFirebase HOC
*/
const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
const [error, setError] = useState<any>(null);
// handle form data
const initFormData: InitFormData = {
name: '',
email: '',
password: '',
password2: '',
};
const {formData, handleChange, resetForm} = useForm<InitFormData>(
initFormData,
);
const {name, email, password, password2} = formData;
// prevent submitting invalid forms
const isDisabled: boolean = name === '' || email === '' || password === '';
/** create user with password */
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// pass the info to store into the second argument
firebase
.createUser({email, password}, newUser(name, email))
.then(() => resetForm())
.catch(err => setError(err));
};
const loginWithGoogle = () =>
firebase.login({provider: 'google', type: 'popup'});
// redirect to dashboard if connected
if (isLoaded && !isEmpty) {
return <Redirect to={ROUTES.DASHBOARD} />;
}
return (
<section className="container">
{error && <Alert text={error?.message} />}
<Header title="Sign Up" lead="Create your account" />
<GoogleButton type="light" className="my-1" onClick={loginWithGoogle} />
<form className="form" onSubmit={handleSubmit}>
<div className="form-group">
<input
name="name"
value={name}
onChange={handleChange}
placeholder="Name"
type="text"
required
autoFocus
/>
</div>
<div className="form-group">
<input
name="email"
value={email}
onChange={handleChange}
placeholder="Email Address"
type="email"
required
/>
<small className="form-text">
This site uses Gravatar, so use a Gravatar email.
</small>
</div>
<div className="form-group">
<input
name="password"
value={password}
onChange={handleChange}
placeholder="Password"
type="password"
minLength={6}
required
/>
</div>
<div className="form-group">
<input
name="password2"
value={password2}
onChange={handleChange}
placeholder="Confirm Password"
type="password"
minLength={6}
required
/>
</div>
<input
type="submit"
value="Register"
className="btn btn-primary"
disabled={isDisabled}
/>
</form>
<p className="my-1">
Already have an account? <Link to={ROUTES.SIGN_IN}>Sign in</Link>
</p>
</section>
);
};
/** subscribe to store and firebase */
const enhance = compose<FC<IProps>>(connect(selectProfile), withFirebase);
export default enhance(SignUp);

View file

@ -0,0 +1,7 @@
import SignUp from '../SignUp';
describe('Signup Page', () => {
it('calls loadUser function', () => {});
it('redirects to dashboard if signed up', () => {});
it('call signup function on click', () => {});
});

View file

@ -0,0 +1,48 @@
import React, {FC} from 'react';
// Routing
import {Route, Redirect} from 'react-router-dom';
import * as ROUTES from '../constants/routes';
// Redux
import {isLoaded, isEmpty} from 'react-redux-firebase';
import {useSelector} from 'react-redux';
import {RootState} from '../store';
interface IProps {
exact?: boolean;
path: string;
component: React.FC<any>;
}
/**
* Redirects to the login screen if you're not authenticated yet or
* if auth is not loaded yet
*/
const PrivateRoute: FC<IProps> = ({
component: Component,
exact,
path,
...rest
}) => {
const auth = useSelector((state: RootState) => state.firebase.auth);
return (
<Route
exact={exact}
path={path}
{...rest}
render={({location, ...rest}) =>
isLoaded(auth) && !isEmpty(auth) ? (
<Component {...rest} />
) : (
<Redirect
to={{
pathname: ROUTES.SIGN_IN,
state: {from: location},
}}
/>
)
}
/>
);
};
/** subscribe to store and firebase */
export default PrivateRoute;

View file

@ -13,6 +13,7 @@ import PostPage from '../pages/Post';
import Posts from '../pages/Posts';
import NotFound from '../pages/NotFound';
import * as ROUTES from '../constants/routes';
import PrivateRoute from './PrivateRoute';
/** Register navigation paths accessible */
const Router: FC = () => (
@ -22,13 +23,16 @@ const Router: FC = () => (
<Route exact path={ROUTES.SIGN_IN} component={SignIn} />
<Route exact path={ROUTES.DEVELOPERS} component={Developers} />
<Route exact path={ROUTES.PROFILE} component={Profile} />
<Route exact path={ROUTES.EDIT_PROFILE} component={EditProfile} />
<Route exact path={ROUTES.DASHBOARD} component={Dashboard} />
<Route exact path={ROUTES.ADD_EXPERIENCE} component={AddExperience} />
<Route exact path={ROUTES.ADD_EDUCATION} component={AddEducation} />
<Route exact path={ROUTES.POST} component={PostPage} />
<Route exact path={ROUTES.POSTS} component={Posts} />
<Route exact path={ROUTES.POSTS} component={Posts} />
<PrivateRoute exact path={ROUTES.EDIT_PROFILE} component={EditProfile} />
<PrivateRoute exact path={ROUTES.DASHBOARD} component={Dashboard} />
<PrivateRoute
exact
path={ROUTES.ADD_EXPERIENCE}
component={AddExperience}
/>
<PrivateRoute exact path={ROUTES.ADD_EDUCATION} component={AddEducation} />
<PrivateRoute exact path={ROUTES.POST} component={PostPage} />
<PrivateRoute exact path={ROUTES.POSTS} component={Posts} />
<Route component={NotFound} />
</Switch>
);

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
@import "_config";
@import '_config';
// Backgrounds

View file

@ -9558,6 +9558,13 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
react-google-button@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/react-google-button/-/react-google-button-0.7.1.tgz#e84b4ce270a66e345489dc86e47235e877fcc81a"
integrity sha512-FN8/9Va6oAGKL561yddNzcjz0iGlt2GZFwiDPczEu0fDVnR21/nBrPs7Y5x97V1S4//nvtkvv6ohfwtxIHpbfg==
dependencies:
prop-types "^15.7.2"
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"