📊Dashboard (#7)

* connect Dashboard to store

* delete account button logs out...

* buttons routing functional

* refactor enhance to store

* use an enum for routes and statuses

* add statuses enum and enable EditProfile Form

* conditional display of social links

* Links type,

* display alert on EditProfile form submit

* refactor extract alert interface

* update useForm hook to handle checkboxes

* enable add education form

* enable add experience form

* add blank dev Profile on signup

* enable delete credential button

* delete account set profile to inactive

* add isActive field to dev, checks for user existance on sign up to not overwrite inactive profiles
This commit is contained in:
Ruidy 2020-05-16 14:17:37 +02:00 committed by GitHub
parent 07dd7c5624
commit 9e30322ffc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 875 additions and 256 deletions

View file

@ -1,58 +1,58 @@
import * as ROUTES from '../../src/constants/routes'; import Routes from '../../src/constants/Routes';
describe('App Router', () => { describe('App Router', () => {
it('contains Landing page', () => { it('contains Landing page', () => {
cy.visit(ROUTES.LANDING); cy.visit(Routes.LANDING);
cy.get('section'); cy.get('section');
}); });
it('contains SignUp page', () => { it('contains SignUp page', () => {
cy.visit(ROUTES.SIGN_UP); cy.visit(Routes.SIGN_UP);
cy.get('section'); cy.get('section');
}); });
it('contains SignIn page', () => { it('contains SignIn page', () => {
cy.visit(ROUTES.SIGN_IN); cy.visit(Routes.SIGN_IN);
cy.get('section'); cy.get('section');
}); });
it('contains Developers page', () => { it('contains Developers page', () => {
cy.visit(ROUTES.DEVELOPERS); cy.visit(Routes.DEVELOPERS);
cy.get('section'); cy.get('section');
}); });
it('contains Profile page', () => { it('contains Profile page', () => {
cy.visit(ROUTES.PROFILE); cy.visit(Routes.PROFILE);
cy.get('section'); cy.get('section');
}); });
it('contains Edit Profile page', () => { it('contains Edit Profile page', () => {
cy.visit(ROUTES.EDIT_PROFILE); cy.visit(Routes.EDIT_PROFILE);
cy.get('section'); cy.get('section');
}); });
it('contains Add Experience page', () => { it('contains Add Experience page', () => {
cy.visit(ROUTES.ADD_EXPERIENCE); cy.visit(Routes.ADD_EXPERIENCE);
cy.get('section'); cy.get('section');
}); });
it('contains Add Education page', () => { it('contains Add Education page', () => {
cy.visit(ROUTES.ADD_EDUCATION); cy.visit(Routes.ADD_EDUCATION);
cy.get('section'); cy.get('section');
}); });
it('contains Dashboard page', () => { it('contains Dashboard page', () => {
cy.visit(ROUTES.DASHBOARD); cy.visit(Routes.DASHBOARD);
cy.get('section'); cy.get('section');
}); });
it('contains Post page', () => { it('contains Post page', () => {
cy.visit(ROUTES.POST); cy.visit(Routes.POST);
cy.get('section'); cy.get('section');
}); });
it('contains Posts page', () => { it('contains Posts page', () => {
cy.visit(ROUTES.POSTS); cy.visit(Routes.POSTS);
cy.get('section'); cy.get('section');
}); });
}); });

View file

@ -30,7 +30,7 @@
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"redux-firestore": "^0.13.0", "redux-firestore": "^0.13.0",
"typescript": "~3.7.2" "typescript": "^3.9.2"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View file

@ -4,10 +4,11 @@ import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons';
interface IProps { interface IProps {
text: string; text: string;
color?: string;
} }
const Alert: FC<IProps> = ({text}) => ( const Alert: FC<IProps> = ({text, color = 'danger'}) => (
<div className="alert alert-danger"> <div className={`alert alert-${color}`}>
<FontAwesomeIcon icon={faExclamationTriangle} /> {text} <FontAwesomeIcon icon={faExclamationTriangle} /> {text}
</div> </div>
); );

View file

@ -9,16 +9,16 @@ import {DevSummary} from '../models/Dev';
*/ */
const DevProfile: FC<DevSummary> = ({ const DevProfile: FC<DevSummary> = ({
id, id,
name, displayName,
picture, picture,
description, description,
location, location,
skills, skills,
}) => ( }) => (
<div className="profile bg-light"> <div className="profile bg-light">
<img src={picture} alt={name} className="round-img" /> <img src={picture} alt={displayName} className="round-img" />
<div> <div>
<h2>{name}</h2> <h2>{displayName}</h2>
<p>{description}</p> <p>{description}</p>
<p>{location}</p> <p>{location}</p>
<a href="profile.html" className="btn btn-primary"> <a href="profile.html" className="btn btn-primary">

View file

@ -1,41 +1,40 @@
import React, {FC} from 'react'; import React, {FC} from 'react';
// Routing // Routing
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import * as ROUTES from '../constants/routes'; import Routes from '../constants/routes';
//Redux //Redux
import {compose} from '@reduxjs/toolkit'; import {WithFirebaseProps} from 'react-redux-firebase';
import {connect} from 'react-redux'; import {enhance} from '../store/firebase';
import {withFirebase, WithFirebaseProps} from 'react-redux-firebase';
import {selectProfile} from '../store/firebase';
// Style // Style
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCode, faSignOutAlt, faUser} from '@fortawesome/free-solid-svg-icons'; import {faCode, faSignOutAlt, faUser} from '@fortawesome/free-solid-svg-icons';
// Typing // Typing
import User from '../models/User'; import User from '../models/User';
import Dev from '../models/Dev';
interface IProps extends WithFirebaseProps<User> { interface IProps extends Dev, WithFirebaseProps<User> {
isEmpty: boolean; isEmpty: boolean;
isLoaded: boolean; isLoaded: boolean;
} }
/** /**
* Main Navbar serves navigation routes. * Main Navbar serves navigation Routes.
*/ */
const NavBar: FC<IProps> = ({firebase, isEmpty, isLoaded}) => { const NavBar: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
const publicLinks = ( const publicLinks = (
<ul data-testid="publicLinks"> <ul data-testid="publicLinks">
<li> <li>
<Link to={ROUTES.DEVELOPERS} data-testid="devsLink"> <Link to={Routes.DEVELOPERS} data-testid="devsLink">
Developers Developers
</Link> </Link>
</li> </li>
<li> <li>
<Link to={ROUTES.SIGN_UP} data-testid="signupLink"> <Link to={Routes.SIGN_UP} data-testid="signupLink">
Register Register
</Link> </Link>
</li> </li>
<li> <li>
<Link to={ROUTES.SIGN_IN} data-testid="loginLink"> <Link to={Routes.SIGN_IN} data-testid="loginLink">
Login Login
</Link> </Link>
</li> </li>
@ -45,24 +44,24 @@ const NavBar: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
const privateLinks = ( const privateLinks = (
<ul data-testid="privateLinks"> <ul data-testid="privateLinks">
<li> <li>
<Link to={ROUTES.DEVELOPERS} data-testid="devsLink"> <Link to={Routes.DEVELOPERS} data-testid="devsLink">
Developers Developers
</Link> </Link>
</li> </li>
<li> <li>
<Link to={ROUTES.POSTS} data-testid="postsLink"> <Link to={Routes.POSTS} data-testid="postsLink">
Posts Posts
</Link> </Link>
</li> </li>
<li> <li>
<Link to={ROUTES.DASHBOARD} data-testid="dashboardLink"> <Link to={Routes.DASHBOARD} data-testid="dashboardLink">
<FontAwesomeIcon icon={faUser} /> <FontAwesomeIcon icon={faUser} />
<span className="hide-sm"> Dashboard</span> <span className="hide-sm"> Dashboard</span>
</Link> </Link>
</li> </li>
<li> <li>
<Link <Link
to={ROUTES.SIGN_IN} to={Routes.SIGN_IN}
data-testid="logoutLink" data-testid="logoutLink"
onClick={() => firebase.logout()} onClick={() => firebase.logout()}
> >
@ -74,12 +73,13 @@ const NavBar: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
); );
/** Display appropriated links after loading given authenticated prop */ /** Display appropriated links after loading given authenticated prop */
const RenderLinks = isLoaded && !isEmpty ? privateLinks : publicLinks; const RenderLinks =
isLoaded && !isEmpty && isActive ? privateLinks : publicLinks;
return ( return (
<nav className="navbar bg-dark"> <nav className="navbar bg-dark">
<h1> <h1>
<Link to={ROUTES.LANDING} data-testid="homeLink"> <Link to={Routes.LANDING} data-testid="homeLink">
<FontAwesomeIcon icon={faCode} /> DevBook <FontAwesomeIcon icon={faCode} /> DevBook
</Link> </Link>
</h1> </h1>
@ -89,6 +89,4 @@ const NavBar: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
}; };
/** connect HOC subscribes to the store */ /** connect HOC subscribes to the store */
const enhance = compose<FC>(connect(selectProfile), withFirebase);
export default enhance(NavBar); export default enhance(NavBar);

View file

@ -1,15 +1,19 @@
/** /**
* Register all routes here for easy future modification. * Register all Routes here for easy future modification.
* Paths must start with '/' * Paths must start with '/'
*/ */
export const LANDING: string = '/'; enum Routes {
export const SIGN_UP: string = '/signup'; LANDING = '/',
export const SIGN_IN: string = '/signin'; SIGN_UP = '/signup',
export const DEVELOPERS: string = '/developers'; SIGN_IN = '/signin',
export const PROFILE: string = '/profile'; DEVELOPERS = '/developers',
export const EDIT_PROFILE: string = '/edit-profile'; PROFILE = '/profile',
export const DASHBOARD: string = '/dashboard'; EDIT_PROFILE = '/edit-profile',
export const ADD_EXPERIENCE: string = '/add-experience'; DASHBOARD = '/dashboard',
export const ADD_EDUCATION: string = '/add-education'; ADD_EXPERIENCE = '/add-experience',
export const POST: string = '/post'; ADD_EDUCATION = '/add-education',
export const POSTS: string = '/posts'; POST = '/post',
POSTS = '/posts',
}
export default Routes;

12
src/constants/statuses.ts Normal file
View file

@ -0,0 +1,12 @@
const Statuses: string[] = [
'Developer',
'Junior Developer',
'Senior Developer',
'Manager',
'Student or Learning',
'Instructor or Teacher',
'Intern',
'Other',
];
export default Statuses;

View file

@ -9,11 +9,13 @@ import {useState, ChangeEvent} from 'react';
* @returns handleChange function to pass to input tag * @returns handleChange function to pass to input tag
* @returns resetForm function to revert to initFormData * @returns resetForm function to revert to initFormData
* */ * */
const useForm = <T,>(initFormData: T) => { const useForm = <T>(initFormData: T) => {
const [formData, setFormData] = useState<T>(initFormData); const [formData, setFormData] = useState<T>(initFormData);
/** update each input state value onChange */ /** update each input state value onChange */
const handleChange = (e: ChangeEvent<HTMLInputElement>): void => const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
): void =>
setFormData({ setFormData({
...formData, ...formData,
[e.target.name]: e.target.value, [e.target.name]: e.target.value,
@ -22,7 +24,11 @@ const useForm = <T,>(initFormData: T) => {
/** clean form after successful submition */ /** clean form after successful submition */
const resetForm = () => setFormData(initFormData); const resetForm = () => setFormData(initFormData);
return {formData, handleChange, resetForm}; // /** update checkboxes TODO: do it better ...*/
const handleCheckboxesChange = (e: ChangeEvent<HTMLInputElement>): void =>
setFormData({...formData, [e.target.name]: e.target.checked});
return {formData, handleChange, handleCheckboxesChange, resetForm};
}; };
export default useForm; export default useForm;

View file

@ -1,11 +1,12 @@
import Experience from '../types/Experience';
import Education from '../types/Education'; import Education from '../types/Education';
import Experience from '../types/Experience';
import Links from '../types/Links';
import Repo from '../types/Repo'; import Repo from '../types/Repo';
/** Shorter dev interface */ /** Shorter dev interface */
export interface DevSummary { export interface DevSummary {
id: string; id: string;
name: string; displayName: string;
picture: string; picture: string;
description: string; description: string;
location: string; location: string;
@ -16,45 +17,86 @@ export interface DevSummary {
* @extends DevSummary to avoid duplication * @extends DevSummary to avoid duplication
*/ */
interface Dev extends DevSummary { interface Dev extends DevSummary {
isActive: boolean;
bio: string; bio: string;
links: Object; status: string;
company: string;
links: Links;
experiences: Experience[]; experiences: Experience[];
educations: Education[]; educations: Education[];
repos: Repo[]; repos: Repo[];
} }
/** create profile tagline */
export const getDescription = (status: string, company: string) =>
`${status} at ${company}`;
/** blank Dev serve as placeholder when initializing a new profile */
export const blankDev: Dev = {
id: '42',
isActive: true,
displayName: '',
status: 'Developer',
company: '',
picture: '',
description: '',
location: '',
skills: [],
links: {
website: '',
instagram: '',
facebook: '',
linkedin: '',
twitter: '',
github: '',
youtube: '',
},
bio: '',
experiences: [],
educations: [],
repos: [],
};
/** /**
* sample Dev for development and tests * sample Dev for development and tests
*/ */
export const dummyDev: Dev = { export const dummyDev: Dev = {
id: '0', id: '0',
name: 'John Doe', isActive: true,
displayName: 'John Doe',
status: 'Developer',
company: 'Microsoft',
picture: picture:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200', 'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
description: 'Developer at Microsoft', description: 'Developer at Microsoft',
location: 'Seattle, WA', location: 'Seattle, WA',
skills: ['HTML', 'CSS', 'JavaScript', 'Python'], skills: ['HTML', 'CSS', 'JavaScript', 'Python'],
links: { links: {
web: '#', website: '#',
instagram: 'http://insta.com', instagram: 'http://insta.com',
facebook: '#', facebook: '#',
linkedin: '#', linkedin: '#',
twitter: '#', twitter: '#',
github: '#', github: '#',
youtube: '#',
}, },
bio: bio:
'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Blanditiis unde quae vero enim adipisci voluptas magni sapiente reprehenderit error minima.', 'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Blanditiis unde quae vero enim adipisci voluptas magni sapiente reprehenderit error minima.',
experiences: [ experiences: [
{ {
id: 1,
company: 'Microsoft', company: 'Microsoft',
from: new Date(2011, 10), from: new Date(2011, 10),
to: 'Current', to: 'Current',
position: 'Senior Developer', position: 'Senior Developer',
location: 'USA',
description: description:
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptas corrupti rem eius, accusantium ipsum vel eveniet magnam voluptatum? Minus, voluptatum!', 'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptas corrupti rem eius, accusantium ipsum vel eveniet magnam voluptatum? Minus, voluptatum!',
}, },
{ {
id: 0,
company: 'Sun Microsystems', company: 'Sun Microsystems',
location: 'USA',
from: new Date(2004, 10), from: new Date(2004, 10),
to: new Date(2010, 11), to: new Date(2010, 11),
position: 'System Admin', position: 'System Admin',
@ -64,6 +106,7 @@ export const dummyDev: Dev = {
], ],
educations: [ educations: [
{ {
id: 0,
school: 'University of Washington', school: 'University of Washington',
from: new Date(1993, 9), from: new Date(1993, 9),
to: new Date(1999, 6), to: new Date(1999, 6),

View file

@ -1,10 +1,97 @@
import React, {FC} from 'react'; import React, {FC, useState, FormEvent} from 'react';
// Routing
import {Link} from 'react-router-dom';
import Routes from '../constants/routes';
// Redux
import {WithFirebaseProps} from 'react-redux-firebase';
import {enhance} from '../store/firebase';
// Style
import FormHeader from '../components/FormHeader'; import FormHeader from '../components/FormHeader';
import Alert from '../components/Alert';
// Typing
import Dev from '../models/Dev';
import User from '../models/User';
import IAlert, {formAlert} from '../types/Alert';
import Education from '../types/Education';
import {parseDate} from '../types/TimePeriod';
// Form
import useForm from '../hooks';
interface FormData {
school: string;
degree: string;
field: string;
from: string;
to: string;
current: boolean;
description: string;
}
interface IProps extends Dev, WithFirebaseProps<User> {}
/** /**
* Form to add an Education step to Profile * Form to add an Education step to Profile
*/ */
const AddEducation: FC = () => ( const AddEducation: FC<IProps> = ({firebase, educations}) => {
const [alert, setAlert] = useState<IAlert>(formAlert);
const initFormData: FormData = {
school: '',
degree: '',
field: '',
from: '',
to: '',
current: false,
description: '',
};
const {formData, handleChange, handleCheckboxesChange, resetForm} = useForm<
FormData
>(initFormData);
const isDisabled: boolean = formData.school === '' || formData.degree === '';
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const makeEducation = ({
school,
degree,
from,
field,
to,
current,
description,
}: FormData): Education => {
if (current) to = 'Current';
const newEdu: Education = {
id: educations.length,
school,
degree,
field,
from: parseDate(from),
to: parseDate(to),
description,
};
return newEdu;
};
const newEdu = makeEducation(formData);
try {
firebase.updateProfile(
{educations: [...educations, newEdu]},
{useSet: true, merge: true},
);
setAlert({
show: true,
color: 'success',
text:
'Profile successfully updated. You may continue or go back to your dashboard.',
});
resetForm();
} catch (err) {
setAlert({...alert, show: true});
}
};
return (
<section className="container"> <section className="container">
<FormHeader <FormHeader
title="Add Your Education" title="Add Your Education"
@ -13,13 +100,16 @@ const AddEducation: FC = () => (
icon="graduation-cap" icon="graduation-cap"
/> />
<form className="form"> <form className="form" onSubmit={handleSubmit}>
<div className="form-group"> <div className="form-group">
<input <input
type="text" type="text"
placeholder="* School or Bootcamp" placeholder="* School or Bootcamp"
name="school" name="school"
value={formData.school}
onChange={handleChange}
required required
autoFocus
/> />
</div> </div>
<div className="form-group"> <div className="form-group">
@ -27,23 +117,47 @@ const AddEducation: FC = () => (
type="text" type="text"
placeholder="* Degree or Certificate" placeholder="* Degree or Certificate"
name="degree" name="degree"
value={formData.degree}
onChange={handleChange}
required required
/> />
</div> </div>
<div className="form-group"> <div className="form-group">
<input type="text" placeholder="Field Of Study" name="fieldofstudy" /> <input
type="text"
placeholder="Field Of Study"
name="field"
value={formData.field}
onChange={handleChange}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<h4>From Date</h4> <h4>From Date</h4>
<input type="date" name="from" /> <input
type="date"
name="from"
value={formData.from}
onChange={handleChange}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<h4>To Date</h4> <h4>To Date</h4>
<input type="date" name="to" /> <input
type="date"
name="to"
value={formData.to}
onChange={handleChange}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<p> <p>
<input type="checkbox" name="current" value="" /> Current School <input
type="checkbox"
name="current"
checked={formData.current}
onChange={handleCheckboxesChange}
/>{' '}
Current School
</p> </p>
</div> </div>
<div className="form-group"> <div className="form-group">
@ -52,14 +166,23 @@ const AddEducation: FC = () => (
cols={30} cols={30}
rows={5} rows={5}
placeholder="Program Description" placeholder="Program Description"
value={formData.description}
onChange={handleChange}
></textarea> ></textarea>
</div> </div>
<input type="submit" className="btn btn-primary my-1" value="Submit" /> {alert.show && <Alert text={alert.text} color={alert.color} />}
<a className="btn btn-light my-1" href="dashboard.html"> <input
type="submit"
className="btn btn-primary my-1"
value="Submit"
disabled={isDisabled}
/>
<Link className="btn btn-light my-1" to={Routes.DASHBOARD}>
Go Back Go Back
</a> </Link>
</form> </form>
</section> </section>
); );
};
export default AddEducation; export default enhance(AddEducation);

View file

@ -1,10 +1,98 @@
import React, {FC} from 'react'; import React, {FC, useState, FormEvent} from 'react';
// Routing
import {Link} from 'react-router-dom';
import Routes from '../constants/routes';
// Redux
import {WithFirebaseProps} from 'react-redux-firebase';
import {enhance} from '../store/firebase';
// Style
import FormHeader from '../components/FormHeader'; import FormHeader from '../components/FormHeader';
import Alert from '../components/Alert';
// Typing
import Dev from '../models/Dev';
import User from '../models/User';
import IAlert, {formAlert} from '../types/Alert';
import Experience from '../types/Experience';
import {parseDate} from '../types/TimePeriod';
// Form
import useForm from '../hooks';
interface FormData {
position: string;
company: string;
location: string;
from: string;
to: string;
current: boolean;
description: string;
}
interface IProps extends Dev, WithFirebaseProps<User> {}
/** /**
* Form to add an Education step to Profile * Form to add an Experience step to Profile
*/ */
const AddExperience: FC = () => { const AddExperience: FC<IProps> = ({firebase, experiences}) => {
const [alert, setAlert] = useState<IAlert>(formAlert);
const initFormData: FormData = {
position: '',
company: '',
location: '',
from: '',
to: '',
current: false,
description: '',
};
const {formData, handleChange, handleCheckboxesChange, resetForm} = useForm<
FormData
>(initFormData);
const isDisabled: boolean =
formData.position === '' || formData.company === '';
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const makeExperience = ({
position,
company,
from,
location,
to,
current,
description,
}: FormData): Experience => {
if (current) to = 'Current';
const newExp: Experience = {
id: experiences.length,
position,
company,
location,
from: parseDate(from),
to: parseDate(to),
description,
};
return newExp;
};
const newExp = makeExperience(formData);
try {
firebase.updateProfile(
{experiences: [...experiences, newExp]},
{useSet: true, merge: true},
);
setAlert({
show: true,
color: 'success',
text:
'Profile successfully updated. You may continue or go back to your dashboard.',
});
resetForm();
} catch (err) {
setAlert({...alert, show: true});
}
};
return ( return (
<section className="container"> <section className="container">
<FormHeader <FormHeader
@ -14,27 +102,63 @@ const AddExperience: FC = () => {
icon="code-branch" icon="code-branch"
/> />
<form className="form"> <form className="form" onSubmit={handleSubmit}>
<div className="form-group"> <div className="form-group">
<input type="text" placeholder="* Job Title" name="title" required /> <input
type="text"
placeholder="* Job Title"
name="position"
required
value={formData.position}
onChange={handleChange}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<input type="text" placeholder="* Company" name="company" required /> <input
type="text"
placeholder="* Company"
name="company"
required
value={formData.company}
onChange={handleChange}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<input type="text" placeholder="Location" name="location" /> <input
type="text"
placeholder="Location"
name="location"
value={formData.location}
onChange={handleChange}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<h4>From Date</h4> <h4>From Date</h4>
<input type="date" name="from" /> <input
type="date"
name="from"
value={formData.from}
onChange={handleChange}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<h4>To Date</h4> <h4>To Date</h4>
<input type="date" name="to" /> <input
type="date"
name="to"
value={formData.to}
onChange={handleChange}
/>
</div> </div>
<div className="form-group"> <div className="form-group">
<p> <p>
<input type="checkbox" name="current" value="" /> Current Job <input
type="checkbox"
name="current"
checked={formData.current}
onChange={handleCheckboxesChange}
/>{' '}
Current Job
</p> </p>
</div> </div>
<div className="form-group"> <div className="form-group">
@ -43,15 +167,23 @@ const AddExperience: FC = () => {
cols={30} cols={30}
rows={5} rows={5}
placeholder="Job Description" placeholder="Job Description"
value={formData.description}
onChange={handleChange}
></textarea> ></textarea>
</div> </div>
<input type="submit" className="btn btn-primary my-1" value="Submit" /> {alert.show && <Alert text={alert.text} color={alert.color} />}
<a className="btn btn-light my-1" href="dashboard.html"> <input
type="submit"
className="btn btn-primary my-1"
value="Submit"
disabled={isDisabled}
/>
<Link className="btn btn-light my-1" to={Routes.DASHBOARD}>
Go Back Go Back
</a> </Link>
</form> </form>
</section> </section>
); );
}; };
export default AddExperience; export default enhance(AddExperience);

View file

@ -1,4 +1,11 @@
import React, {FC} from 'react'; import React, {FC, MouseEvent} from 'react';
// Redux
import {WithFirebaseProps} from 'react-redux-firebase';
import {enhance} from '../store/firebase';
// Routing
import {Link} from 'react-router-dom';
import Routes from '../constants/routes';
// Style
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import { import {
faUserCircle, faUserCircle,
@ -7,28 +14,67 @@ import {
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import {faBlackTie} from '@fortawesome/free-brands-svg-icons'; import {faBlackTie} from '@fortawesome/free-brands-svg-icons';
import Header from '../components/Header'; import Header from '../components/Header';
import Dev, {dummyDev as dev} from '../models/Dev'; // Types
import Dev from '../models/Dev';
import User from '../models/User';
import Experience from '../types/Experience'; import Experience from '../types/Experience';
import {getTimePeriod} from '../types/TimePeriod'; import {getTimePeriod} from '../types/TimePeriod';
import Education from '../types/Education'; import Education from '../types/Education';
interface IProps extends Dev, WithFirebaseProps<User> {}
/** /**
* Main page from which a Dev can peek and edit its own profile. * Main page from which a Dev can peek and edit its own profile.
*/ */
const Dashboard: FC<Dev> = () => { const Dashboard: FC<IProps> = ({
firebase,
displayName,
experiences,
educations,
}) => {
/** turns account to inactive then logs user out */
const deleteAccount = () => {
firebase.updateProfile({isActive: false}, {useSet: true, merge: true});
firebase.logout();
};
/**
*
* @param id key of the entry to remove
* @param entries array of credential educations
*/
const deleteEduEntry = (id: number, entries: Education[]) => (
e: MouseEvent<HTMLButtonElement>,
) => {
firebase.updateProfile({
educations: entries.filter((e: Education) => e.id !== id),
});
};
/**
*
* @param id key of the entry to remove
* @param entries array of credential experiences
*/
const deleteExpEntry = (id: number, entries: Experience[]) => (
e: MouseEvent<HTMLButtonElement>,
) => {
firebase.updateProfile({
experiences: entries.filter((e: Experience) => e.id !== id),
});
};
return ( return (
<section className="container"> <section className="container">
<Header title="Dashboard" lead={`Welcome ${dev.name}`} /> <Header title="Dashboard" lead={`Welcome ${displayName}`} />
<div className="dash-buttons"> <div className="dash-buttons">
<a href="create-profile.html" className="btn btn-light"> <Link to={Routes.EDIT_PROFILE} className="btn btn-light">
<FontAwesomeIcon icon={faUserCircle} /> Edit Profile <FontAwesomeIcon icon={faUserCircle} /> Edit Profile
</a> </Link>
<a href="add-experience.html" className="btn btn-light"> <Link to={Routes.ADD_EXPERIENCE} className="btn btn-light">
<FontAwesomeIcon icon={faBlackTie} /> Add Experience <FontAwesomeIcon icon={faBlackTie} /> Add Experience
</a> </Link>
<a href="add-education.html" className="btn btn-light"> <Link to={Routes.ADD_EDUCATION} className="btn btn-light">
<FontAwesomeIcon icon={faGraduationCap} /> Add Education <FontAwesomeIcon icon={faGraduationCap} /> Add Education
</a> </Link>
</div> </div>
<h2 className="my-2">Experience Credentials</h2> <h2 className="my-2">Experience Credentials</h2>
@ -42,13 +88,18 @@ const Dashboard: FC<Dev> = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{dev.experiences.map((exp: Experience, i: number) => ( {experiences?.map((exp: Experience) => (
<tr key={i}> <tr key={exp.id}>
<td>{exp.company}</td> <td>{exp.company}</td>
<td className="hide-sm">{exp.position}</td> <td className="hide-sm">{exp.position}</td>
<td className="hide-sm">{getTimePeriod(exp.from, exp.to)}</td> <td className="hide-sm">{getTimePeriod(exp.from, exp.to)}</td>
<td> <td>
<button className="btn btn-danger">Delete</button> <button
className="btn btn-danger"
onClick={deleteExpEntry(exp.id, experiences)}
>
Delete
</button>
</td> </td>
</tr> </tr>
))} ))}
@ -66,20 +117,25 @@ const Dashboard: FC<Dev> = () => {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{dev.educations.map((edu: Education, i: number) => ( {educations?.map((edu: Education, i: number) => (
<tr key={i}> <tr key={edu.id}>
<td>{edu.school}</td> <td>{edu.school}</td>
<td className="hide-sm">{edu.field}</td> <td className="hide-sm">{edu.degree}</td>
<td className="hide-sm">{getTimePeriod(edu.from, edu.to)}</td> <td className="hide-sm">{getTimePeriod(edu.from, edu.to)}</td>
<td> <td>
<button className="btn btn-danger">Delete</button> <button
className="btn btn-danger"
onClick={deleteEduEntry(edu.id, educations)}
>
Delete
</button>
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
<div className="my-2"> <div className="my-2">
<button className="btn btn-danger"> <button className="btn btn-danger" onClick={deleteAccount}>
<FontAwesomeIcon icon={faUserSlash} /> Delete my Account <FontAwesomeIcon icon={faUserSlash} /> Delete my Account
</button> </button>
</div> </div>
@ -87,4 +143,4 @@ const Dashboard: FC<Dev> = () => {
); );
}; };
export default Dashboard; export default enhance(Dashboard);

View file

@ -11,7 +11,7 @@ const Developers: FC = () => {
const developers: DevSummary[] = [ const developers: DevSummary[] = [
{ {
id: '0', id: '0',
name: 'John Doe', displayName: 'John Doe',
picture: picture:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200', 'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
description: 'Developer at Microsoft', description: 'Developer at Microsoft',
@ -20,7 +20,7 @@ const Developers: FC = () => {
}, },
{ {
id: '42', id: '42',
name: 'Ruidy Nemausat', displayName: 'Ruidy Nemausat',
picture: picture:
'https://lh3.googleusercontent.com/a-/AOh14GhncH95MWKwPR3TRKy4eVd4n6w0-fobe4dhiam2xA', 'https://lh3.googleusercontent.com/a-/AOh14GhncH95MWKwPR3TRKy4eVd4n6w0-fobe4dhiam2xA',
description: 'Fullstack Engineer at DESY', description: 'Fullstack Engineer at DESY',

View file

@ -1,4 +1,10 @@
import React, {FC} from 'react'; import React, {FC, useState} from 'react';
import {Link} from 'react-router-dom';
import Routes from '../constants/routes';
// Redux
import {enhance} from '../store/firebase';
import {WithFirebaseProps} from 'react-redux-firebase';
// Style
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import { import {
faTwitter, faTwitter,
@ -8,55 +14,191 @@ import {
faInstagram, faInstagram,
} from '@fortawesome/free-brands-svg-icons'; } from '@fortawesome/free-brands-svg-icons';
import FormHeader from '../components/FormHeader'; import FormHeader from '../components/FormHeader';
import Alert from '../components/Alert';
import Statuses from '../constants/statuses';
// Form
import useForm from '../hooks';
// Typing
import Dev from '../models/Dev';
import User from '../models/User';
import Links from '../types/Links';
import IAlert, {formAlert} from '../types/Alert';
interface FormData {
status: string;
company: string;
website: string;
location: string;
skills: string;
github: string;
bio: string;
facebook: string;
linkedin: string;
instagram: string;
twitter: string;
youtube: string;
}
interface IProps extends Dev, WithFirebaseProps<User> {}
/** /**
* Form to update dev's personal information. * Form to update dev's personal information.
*/ */
const EditProfile: FC = () => { const EditProfile: FC<IProps> = ({
firebase,
status,
skills,
company,
links,
location,
bio,
}) => {
const [showLinks, setShowLinks] = useState(false);
const [alert, setAlert] = useState<IAlert>(formAlert);
const initFormData = {
status: status ?? 'Developer',
company: company,
location: location ?? '',
bio: bio ?? '',
skills: skills?.toString() ?? '',
website: links?.website ?? '',
github: links?.github ?? '',
facebook: links?.facebook ?? '',
linkedin: links?.linkedin ?? '',
instagram: links?.instagram ?? '',
twitter: links?.twitter ?? '',
youtube: links?.youtube ?? '',
};
const {formData, handleChange} = useForm<FormData>(initFormData);
/** construct profile object from formData */
const makeProfile = ({
status,
company,
location,
bio,
website,
instagram,
facebook,
linkedin,
twitter,
github,
youtube,
skills,
}: FormData) => {
const newLinks: Links = {
website,
instagram,
facebook,
linkedin,
twitter,
github,
youtube,
};
const newSkills: string[] = skills?.split(',');
return {
status,
company,
location,
bio,
links: newLinks,
skills: newSkills,
};
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const updatedDev = makeProfile(formData);
try {
firebase.updateProfile(updatedDev, {useSet: true, merge: true});
setAlert({
show: true,
color: 'success',
text:
'Profile successfully updated. You may go back to your dashboard.',
});
} catch (err) {
setAlert({...alert, show: true});
}
};
const isDisabled: boolean = formData.status === '' || formData.skills === '';
const toggleSocialLinks = () => setShowLinks(!showLinks);
return ( return (
<section className="container"> <section className="container">
<FormHeader <FormHeader
title="Create your profile" title="Edit your profile"
lead="Let's get some information to make your profile stand out" lead="Let's get some information to make your profile stand out"
/> />
<form className="form"> <form className="form" onSubmit={handleSubmit}>
<div className="form-group"> <div className="form-group">
<select name="status" required> <select
<option value="0">* Select Professional Status</option> name="status"
<option value="Developer">Developer</option> required
<option value="Junior Developer">Junior Developer</option> onChange={handleChange}
<option value="Senior Developer">Senior Developer</option> defaultValue={formData.status}
<option value="Manager">Manager</option> >
<option value="Student or Learning">Student or Learning</option> <option disabled>* Select Professional Status</option>
<option value="Instructor">Instructor or Teacher</option> {Statuses.map((s: string, i: number) => (
<option value="Intern">Intern</option> <option value={s} key={i}>
<option value="Other">Other</option> {s}
</option>
))}
</select> </select>
<small className="form-text"> <small className="form-text">
Give us an idea of where you are at in your career Give us an idea of where you are at in your career
</small> </small>
</div> </div>
<div className="form-group"> <div className="form-group">
<input type="text" placeholder="Company" name="company" /> <input
type="text"
placeholder="Company"
name="company"
value={formData.company}
// value={variable}
onChange={handleChange}
/>
<small className="form-text"> <small className="form-text">
Could be your own company or one you work for Could be your own company or one you work for
</small> </small>
</div> </div>
<div className="form-group"> <div className="form-group">
<input type="text" placeholder="Website" name="website" /> <input
type="text"
placeholder="Website"
name="website"
value={formData.website}
onChange={handleChange}
/>
<small className="form-text"> <small className="form-text">
Could be your own or a company website Could be your own or a company website
</small> </small>
</div> </div>
<div className="form-group"> <div className="form-group">
<input type="text" placeholder="Location" name="location" /> <input
type="text"
placeholder="Location"
name="location"
value={formData.location}
onChange={handleChange}
/>
<small className="form-text"> <small className="form-text">
City & state suggested (eg. Boston, MA) City & state suggested (eg. Boston, MA)
</small> </small>
</div> </div>
<div className="form-group"> <div className="form-group">
<input type="text" placeholder="* Skills" name="skills" required /> <input
type="text"
placeholder="* Skills"
name="skills"
required
value={formData.skills}
onChange={handleChange}
/>
<small className="form-text"> <small className="form-text">
Please use comma separated values (eg. HTML,CSS,JavaScript,PHP) Please use comma separated values (eg. HTML,CSS,JavaScript,PHP)
</small> </small>
@ -65,7 +207,9 @@ const EditProfile: FC = () => {
<input <input
type="text" type="text"
placeholder="Github Username" placeholder="Github Username"
name="githubusername" name="github"
value={formData.github}
onChange={handleChange}
/> />
<small className="form-text"> <small className="form-text">
If you want your latest repos and a Github link, include your If you want your latest repos and a Github link, include your
@ -73,49 +217,97 @@ const EditProfile: FC = () => {
</small> </small>
</div> </div>
<div className="form-group"> <div className="form-group">
<textarea placeholder="A short bio of yourself" name="bio"></textarea> <textarea
placeholder="A short bio of yourself"
name="bio"
value={formData.bio}
onChange={handleChange}
></textarea>
<small className="form-text">Tell us a little about yourself</small> <small className="form-text">Tell us a little about yourself</small>
</div> </div>
<div className="my-2"> <div className="my-2">
<button type="button" className="btn btn-light"> <button
Add Social Network Links type="button"
className="btn btn-light"
onClick={toggleSocialLinks}
>
{showLinks ? 'Hide' : 'Add'} Social Network Links
</button> </button>
<span>Optional</span> <span>Optional</span>
</div> </div>
{showLinks && (
<>
<div className="form-group social-input"> <div className="form-group social-input">
<FontAwesomeIcon icon={faFacebook} size="2x" /> <FontAwesomeIcon icon={faFacebook} size="2x" />
<input type="text" placeholder="Facebook URL" name="facebook" /> <input
type="text"
placeholder="Facebook URL"
name="facebook"
value={formData.facebook}
onChange={handleChange}
/>
</div> </div>
<div className="form-group social-input"> <div className="form-group social-input">
<FontAwesomeIcon icon={faInstagram} size="2x" /> <FontAwesomeIcon icon={faInstagram} size="2x" />
<input type="text" placeholder="Instagram URL" name="instagram" /> <input
type="text"
placeholder="Instagram URL"
name="instagram"
value={formData.instagram}
onChange={handleChange}
/>
</div> </div>
<div className="form-group social-input"> <div className="form-group social-input">
<FontAwesomeIcon icon={faLinkedin} size="2x" /> <FontAwesomeIcon icon={faLinkedin} size="2x" />
<input type="text" placeholder="Linkedin URL" name="linkedin" /> <input
type="text"
placeholder="Linkedin URL"
name="linkedin"
value={formData.linkedin}
onChange={handleChange}
/>
</div> </div>
<div className="form-group social-input"> <div className="form-group social-input">
<FontAwesomeIcon icon={faTwitter} size="2x" /> <FontAwesomeIcon icon={faTwitter} size="2x" />
<input type="text" placeholder="Twitter URL" name="twitter" /> <input
type="text"
placeholder="Twitter URL"
name="twitter"
value={formData.twitter}
onChange={handleChange}
/>
</div> </div>
<div className="form-group social-input"> <div className="form-group social-input">
<FontAwesomeIcon icon={faYoutube} size="2x" /> <FontAwesomeIcon icon={faYoutube} size="2x" />
<input type="text" placeholder="YouTube URL" name="youtube" /> <input
type="text"
placeholder="YouTube URL"
name="youtube"
value={formData.youtube}
onChange={handleChange}
/>
</div> </div>
</>
<input type="submit" className="btn btn-primary my-1" value="Submit" /> )}
<a className="btn btn-light my-1" href="dashboard.html"> {alert.show && <Alert text={alert.text} color={alert.color} />}
<input
type="submit"
className="btn btn-primary my-1"
value="Submit"
disabled={isDisabled}
/>
<Link to={Routes.DASHBOARD} className="btn btn-light my-1">
Go Back Go Back
</a> </Link>
</form> </form>
</section> </section>
); );
}; };
export default EditProfile; export default enhance(EditProfile);

View file

@ -1,6 +1,6 @@
import React, {FC} from 'react'; import React, {FC} from 'react';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import * as ROUTES from '../constants/routes'; import Routes from '../constants/routes';
import Header from '../components/Header'; import Header from '../components/Header';
/** /**
@ -16,10 +16,10 @@ const Landing: FC = () => (
icon="code" icon="code"
/> />
<div className="buttons"> <div className="buttons">
<Link to={ROUTES.SIGN_UP} className="btn btn-primary"> <Link to={Routes.SIGN_UP} className="btn btn-primary">
Sign up Sign up
</Link> </Link>
<Link to={ROUTES.SIGN_IN} className="btn btn-light"> <Link to={Routes.SIGN_IN} className="btn btn-light">
Login Login
</Link> </Link>
</div> </div>

View file

@ -1,7 +1,7 @@
import React, {FC} from 'react'; import React, {FC} from 'react';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import Header from '../components/Header'; import Header from '../components/Header';
import * as ROUTES from '../constants/routes'; import Routes from '../constants/routes';
const NotFound: FC = () => ( const NotFound: FC = () => (
<section className="not-found"> <section className="not-found">
@ -13,10 +13,10 @@ const NotFound: FC = () => (
icon="not-found" icon="not-found"
/> />
<div className="buttons"> <div className="buttons">
<Link to={ROUTES.SIGN_UP} className="btn btn-primary"> <Link to={Routes.SIGN_UP} className="btn btn-primary">
Sign up Sign up
</Link> </Link>
<Link to={ROUTES.SIGN_IN} className="btn btn-light"> <Link to={Routes.SIGN_IN} className="btn btn-light">
Login Login
</Link> </Link>
</div> </div>

View file

@ -56,7 +56,7 @@ const Profile: FC<Dev> = () => {
alt="Some guy" alt="Some guy"
className="round-img my-1" className="round-img my-1"
/> />
<h1 className="large">{dev.name}</h1> <h1 className="large">{dev.displayName}</h1>
<p className="lead">{dev.description}</p> <p className="lead">{dev.description}</p>
<p>{dev.location}</p> <p>{dev.location}</p>
<div className="icons my-1"> <div className="icons my-1">
@ -69,7 +69,7 @@ const Profile: FC<Dev> = () => {
</div> </div>
<div className="profile-about bg-light p-2"> <div className="profile-about bg-light p-2">
<h2 className="text-primary">{`${dev.name}'s Bio`}</h2> <h2 className="text-primary">{`${dev.displayName}'s Bio`}</h2>
<p>{dev.bio}</p> <p>{dev.bio}</p>
<div className="line"></div> <div className="line"></div>
<h2 className="text-primary">Skill Set</h2> <h2 className="text-primary">Skill Set</h2>

View file

@ -1,12 +1,10 @@
import React, {FC, useState} from 'react'; import React, {FC, useState} from 'react';
// Redux // Redux
import {compose} from '@reduxjs/toolkit'; import {WithFirebaseProps} from 'react-redux-firebase';
import {connect} from 'react-redux'; import {enhance} from '../store/firebase';
import {WithFirebaseProps, withFirebase} from 'react-redux-firebase';
import {selectProfile} from '../store/firebase';
// Routing // Routing
import {Link, Redirect} from 'react-router-dom'; import {Link, Redirect} from 'react-router-dom';
import * as ROUTES from '../constants/routes'; import Routes from '../constants/routes';
// Style // Style
import GoogleButton from 'react-google-button'; import GoogleButton from 'react-google-button';
import Header from '../components/Header'; import Header from '../components/Header';
@ -15,13 +13,14 @@ import Alert from '../components/Alert';
import User from '../models/User'; import User from '../models/User';
// Form // Form
import useForm from '../hooks'; import useForm from '../hooks';
import Dev from '../models/Dev';
interface InitFormData { interface InitFormData {
email: string; email: string;
password: string; password: string;
} }
interface IProps extends WithFirebaseProps<User> { interface IProps extends Dev, WithFirebaseProps<User> {
isEmpty: boolean; isEmpty: boolean;
isLoaded: boolean; isLoaded: boolean;
} }
@ -29,7 +28,7 @@ interface IProps extends WithFirebaseProps<User> {
/** /**
* Sign in form * Sign in form
*/ */
const SignIn: FC<IProps> = ({firebase, isEmpty, isLoaded}) => { const SignIn: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
const [error, setError] = useState<any>(null); const [error, setError] = useState<any>(null);
// handle form data // handle form data
@ -59,8 +58,8 @@ const SignIn: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
firebase.login({provider: 'google', type: 'popup'}); firebase.login({provider: 'google', type: 'popup'});
// redirect to dashboard if connected // redirect to dashboard if connected
if (isLoaded && !isEmpty) { if (isLoaded && !isEmpty && isActive) {
return <Redirect to={ROUTES.DASHBOARD} />; return <Redirect to={Routes.DASHBOARD} />;
} }
return ( return (
@ -100,13 +99,11 @@ const SignIn: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
/> />
</form> </form>
<p className="my-1"> <p className="my-1">
Don't have an account? <Link to={ROUTES.SIGN_UP}>Sign up</Link> Don't have an account? <Link to={Routes.SIGN_UP}>Sign up</Link>
</p> </p>
</section> </section>
); );
}; };
/** subscribe to store and firebase */ /** subscribe to store and firebase */
const enhance = compose<FC<IProps>>(connect(selectProfile), withFirebase);
export default enhance(SignIn); export default enhance(SignIn);

View file

@ -1,12 +1,10 @@
import React, {FC, useState} from 'react'; import React, {FC, useState} from 'react';
// Routing // Routing
import {Link, Redirect} from 'react-router-dom'; import {Link, Redirect} from 'react-router-dom';
import * as ROUTES from '../constants/routes'; import Routes from '../constants/routes';
// Redux // Redux
import {compose} from 'redux'; import {WithFirebaseProps} from 'react-redux-firebase';
import {connect} from 'react-redux'; import {enhance} from '../store/firebase';
import {withFirebase, WithFirebaseProps} from 'react-redux-firebase';
import {selectProfile} from '../store/firebase';
import User, {newUser} from '../models/User'; import User, {newUser} from '../models/User';
// Style // Style
import GoogleButton from 'react-google-button'; import GoogleButton from 'react-google-button';
@ -14,9 +12,10 @@ import Alert from '../components/Alert';
import Header from '../components/Header'; import Header from '../components/Header';
// Form // Form
import useForm from '../hooks'; import useForm from '../hooks';
import Dev, {blankDev} from '../models/Dev';
// extends withFirebaseProps type to ad profile info // extends withFirebaseProps type to ad profile info
interface IProps extends WithFirebaseProps<User> { interface IProps extends Dev, WithFirebaseProps<User> {
isEmpty: boolean; isEmpty: boolean;
isLoaded: boolean; isLoaded: boolean;
} }
@ -31,7 +30,7 @@ interface InitFormData {
/** /**
* Sign up form recieves firebase from withFirebase HOC * Sign up form recieves firebase from withFirebase HOC
*/ */
const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded}) => { const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
const [error, setError] = useState<any>(null); const [error, setError] = useState<any>(null);
// handle form data // handle form data
@ -57,16 +56,40 @@ const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
// pass the info to store into the second argument // pass the info to store into the second argument
firebase firebase
.createUser({email, password}, newUser(name, email)) .createUser({email, password}, newUser(name, email))
.then(() => resetForm()) .then(() => {
firebase.updateProfile(blankDev, {useSet: true, merge: true});
resetForm();
})
.catch(err => setError(err)); .catch(err => setError(err));
}; };
const loginWithGoogle = () => const loginWithGoogle = () =>
firebase.login({provider: 'google', type: 'popup'}); firebase
.login({provider: 'google', type: 'popup'})
.then(() => {
// updateProfile only if user does not already exists in db
const email = firebase.auth().currentUser?.email;
let exists: boolean = false;
firebase
.firestore()
.collection('users/')
.where('email', '==', email)
.get()
.then(docs =>
docs.forEach(doc => {
exists = doc.data().isActive !== undefined;
}),
)
.then(() => {
if (!exists)
firebase.updateProfile(blankDev, {useSet: true, merge: true});
});
})
.catch(err => setError(err));
// redirect to dashboard if connected // redirect to dashboard if connected
if (isLoaded && !isEmpty) { if (isLoaded && !isEmpty && isActive) {
return <Redirect to={ROUTES.DASHBOARD} />; return <Redirect to={Routes.DASHBOARD} />;
} }
return ( return (
@ -133,13 +156,11 @@ const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
/> />
</form> </form>
<p className="my-1"> <p className="my-1">
Already have an account? <Link to={ROUTES.SIGN_IN}>Sign in</Link> Already have an account? <Link to={Routes.SIGN_IN}>Sign in</Link>
</p> </p>
</section> </section>
); );
}; };
/** subscribe to store and firebase */ /** subscribe to store and firebase */
const enhance = compose<FC<IProps>>(connect(selectProfile), withFirebase);
export default enhance(SignUp); export default enhance(SignUp);

View file

@ -1,7 +1,7 @@
import React, {FC} from 'react'; import React, {FC} from 'react';
// Routing // Routing
import {Route, Redirect} from 'react-router-dom'; import {Route, Redirect} from 'react-router-dom';
import * as ROUTES from '../constants/routes'; import Routes from '../constants/routes';
// Redux // Redux
import {isLoaded, isEmpty} from 'react-redux-firebase'; import {isLoaded, isEmpty} from 'react-redux-firebase';
import {useSelector} from 'react-redux'; import {useSelector} from 'react-redux';
@ -23,18 +23,20 @@ const PrivateRoute: FC<IProps> = ({
...rest ...rest
}) => { }) => {
const auth = useSelector((state: RootState) => state.firebase.auth); const auth = useSelector((state: RootState) => state.firebase.auth);
const profile = useSelector((state: RootState) => state.firebase.profile);
const isActive = profile.isActive;
return ( return (
<Route <Route
exact={exact} exact={exact}
path={path} path={path}
{...rest} {...rest}
render={({location, ...rest}) => render={({location, ...rest}) =>
isLoaded(auth) && !isEmpty(auth) ? ( isLoaded(auth) && !isEmpty(auth) && isActive ? (
<Component {...rest} /> <Component {...rest} />
) : ( ) : (
<Redirect <Redirect
to={{ to={{
pathname: ROUTES.SIGN_IN, pathname: Routes.SIGN_IN,
state: {from: location}, state: {from: location},
}} }}
/> />

View file

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

View file

@ -19,7 +19,7 @@ const rrfProps = {
createFirestoreInstance, createFirestoreInstance,
}; };
// Firestore SChema // Firestore Schema
export interface Schema { export interface Schema {
devs: Dev; devs: Dev;
} }

View file

@ -1,6 +1,13 @@
import {FC} from 'react';
// Redux
import {compose} from '@reduxjs/toolkit';
import {connect} from 'react-redux';
import {withFirebase} from 'react-redux-firebase';
import {RootState} from '..'; import {RootState} from '..';
/** export firebase authentication */ /** export firebase authentication */
export const selectAuthState = (state: RootState) => state.firebase.auth; export const selectAuthState = (state: RootState) => state.firebase.auth;
/** export current user profile */ /** export current user profile */
export const selectProfile = (state: RootState) => state.firebase.profile; export const selectProfile = (state: RootState) => state.firebase.profile;
/** subscribe to firebase and profile */
export const enhance = compose<FC>(connect(selectProfile), withFirebase);

View file

@ -1,16 +1,14 @@
// Redux // Redux
import {configureStore} from '@reduxjs/toolkit'; import {configureStore} from '@reduxjs/toolkit';
// import authReducer from './auth/';
// Firebase // Firebase
import {firebaseReducer, FirebaseReducer} from 'react-redux-firebase'; import {firebaseReducer, FirebaseReducer} from 'react-redux-firebase';
import {firestoreReducer} from 'redux-firestore'; import {firestoreReducer} from 'redux-firestore';
// Typing // Typing
import User from '../models/User';
import {Schema} from './firebase/config'; import {Schema} from './firebase/config';
import Dev from '../models/Dev';
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
// auth: authReducer,
firebase: firebaseReducer, firebase: firebaseReducer,
firestore: firestoreReducer, firestore: firestoreReducer,
}, },
@ -18,7 +16,7 @@ const store = configureStore({
// State type // State type
export interface RootState { export interface RootState {
firebase: FirebaseReducer.Reducer<User, Schema>; firebase: FirebaseReducer.Reducer<Dev, Schema>;
} }
export default store; export default store;

13
src/types/Alert.ts Normal file
View file

@ -0,0 +1,13 @@
interface IAlert {
show: boolean;
color: string;
text: string;
}
export const formAlert: IAlert = {
show: false,
color: 'danger',
text: 'Something went wrong',
};
export default IAlert;

View file

@ -1,10 +1,11 @@
import TimePeriod from '../types/TimePeriod'; import TimePeriod from '../types/TimePeriod';
interface Education { interface Education {
id: number;
school: string; school: string;
degree: string;
from: TimePeriod; from: TimePeriod;
to: TimePeriod; to: TimePeriod;
degree: string;
field: string; field: string;
description: string; description: string;
} }

View file

@ -1,11 +1,13 @@
import TimePeriod from '../types/TimePeriod'; import TimePeriod from '../types/TimePeriod';
interface Experience { interface Experience {
id: number;
company: string; company: string;
from: Date; from: TimePeriod;
to: TimePeriod; to: TimePeriod;
position: string; position: string;
description: string; description: string;
location: string;
} }
export default Experience; export default Experience;

11
src/types/Links.ts Normal file
View file

@ -0,0 +1,11 @@
interface Links {
website: string;
instagram: string;
facebook: string;
linkedin: string;
twitter: string;
github: string;
youtube: string;
}
export default Links;

View file

@ -1,9 +1,9 @@
import moment from 'moment'; import moment from 'moment';
type TimePeriod = Date | 'Current'; type TimePeriod = string | Date | 'Current';
/** format exp date to be used */ /** format exp date to be used */
const parseDate = (date: TimePeriod): string => { export const parseDate = (date: TimePeriod): string => {
if (date === 'Current') { if (date === 'Current') {
return date; return date;
} }

View file

@ -11243,10 +11243,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@~3.7.2: typescript@^3.9.2:
version "3.7.5" version "3.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9"
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==
unicode-canonical-property-names-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4" version "1.0.4"