mirror of
https://github.com/rjNemo/devbook_ts
synced 2026-06-06 02:36:39 +00:00
📊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:
parent
07dd7c5624
commit
9e30322ffc
30 changed files with 875 additions and 256 deletions
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
12
src/constants/statuses.ts
Normal 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;
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ const rrfProps = {
|
||||||
createFirestoreInstance,
|
createFirestoreInstance,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Firestore SChema
|
// Firestore Schema
|
||||||
export interface Schema {
|
export interface Schema {
|
||||||
devs: Dev;
|
devs: Dev;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
13
src/types/Alert.ts
Normal 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;
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
11
src/types/Links.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
interface Links {
|
||||||
|
website: string;
|
||||||
|
instagram: string;
|
||||||
|
facebook: string;
|
||||||
|
linkedin: string;
|
||||||
|
twitter: string;
|
||||||
|
github: string;
|
||||||
|
youtube: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Links;
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue