📊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', () => {
it('contains Landing page', () => {
cy.visit(ROUTES.LANDING);
cy.visit(Routes.LANDING);
cy.get('section');
});
it('contains SignUp page', () => {
cy.visit(ROUTES.SIGN_UP);
cy.visit(Routes.SIGN_UP);
cy.get('section');
});
it('contains SignIn page', () => {
cy.visit(ROUTES.SIGN_IN);
cy.visit(Routes.SIGN_IN);
cy.get('section');
});
it('contains Developers page', () => {
cy.visit(ROUTES.DEVELOPERS);
cy.visit(Routes.DEVELOPERS);
cy.get('section');
});
it('contains Profile page', () => {
cy.visit(ROUTES.PROFILE);
cy.visit(Routes.PROFILE);
cy.get('section');
});
it('contains Edit Profile page', () => {
cy.visit(ROUTES.EDIT_PROFILE);
cy.visit(Routes.EDIT_PROFILE);
cy.get('section');
});
it('contains Add Experience page', () => {
cy.visit(ROUTES.ADD_EXPERIENCE);
cy.visit(Routes.ADD_EXPERIENCE);
cy.get('section');
});
it('contains Add Education page', () => {
cy.visit(ROUTES.ADD_EDUCATION);
cy.visit(Routes.ADD_EDUCATION);
cy.get('section');
});
it('contains Dashboard page', () => {
cy.visit(ROUTES.DASHBOARD);
cy.visit(Routes.DASHBOARD);
cy.get('section');
});
it('contains Post page', () => {
cy.visit(ROUTES.POST);
cy.visit(Routes.POST);
cy.get('section');
});
it('contains Posts page', () => {
cy.visit(ROUTES.POSTS);
cy.visit(Routes.POSTS);
cy.get('section');
});
});

View file

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

View file

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

View file

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

View file

@ -1,41 +1,40 @@
import React, {FC} from 'react';
// Routing
import {Link} from 'react-router-dom';
import * as ROUTES from '../constants/routes';
import Routes from '../constants/routes';
//Redux
import {compose} from '@reduxjs/toolkit';
import {connect} from 'react-redux';
import {withFirebase, WithFirebaseProps} from 'react-redux-firebase';
import {selectProfile} from '../store/firebase';
import {WithFirebaseProps} from 'react-redux-firebase';
import {enhance} from '../store/firebase';
// Style
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCode, faSignOutAlt, faUser} from '@fortawesome/free-solid-svg-icons';
// Typing
import User from '../models/User';
import Dev from '../models/Dev';
interface IProps extends WithFirebaseProps<User> {
interface IProps extends Dev, WithFirebaseProps<User> {
isEmpty: 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 = (
<ul data-testid="publicLinks">
<li>
<Link to={ROUTES.DEVELOPERS} data-testid="devsLink">
<Link to={Routes.DEVELOPERS} data-testid="devsLink">
Developers
</Link>
</li>
<li>
<Link to={ROUTES.SIGN_UP} data-testid="signupLink">
<Link to={Routes.SIGN_UP} data-testid="signupLink">
Register
</Link>
</li>
<li>
<Link to={ROUTES.SIGN_IN} data-testid="loginLink">
<Link to={Routes.SIGN_IN} data-testid="loginLink">
Login
</Link>
</li>
@ -45,24 +44,24 @@ const NavBar: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
const privateLinks = (
<ul data-testid="privateLinks">
<li>
<Link to={ROUTES.DEVELOPERS} data-testid="devsLink">
<Link to={Routes.DEVELOPERS} data-testid="devsLink">
Developers
</Link>
</li>
<li>
<Link to={ROUTES.POSTS} data-testid="postsLink">
<Link to={Routes.POSTS} data-testid="postsLink">
Posts
</Link>
</li>
<li>
<Link to={ROUTES.DASHBOARD} data-testid="dashboardLink">
<Link to={Routes.DASHBOARD} data-testid="dashboardLink">
<FontAwesomeIcon icon={faUser} />
<span className="hide-sm"> Dashboard</span>
</Link>
</li>
<li>
<Link
to={ROUTES.SIGN_IN}
to={Routes.SIGN_IN}
data-testid="logoutLink"
onClick={() => firebase.logout()}
>
@ -74,12 +73,13 @@ const NavBar: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
);
/** Display appropriated links after loading given authenticated prop */
const RenderLinks = isLoaded && !isEmpty ? privateLinks : publicLinks;
const RenderLinks =
isLoaded && !isEmpty && isActive ? privateLinks : publicLinks;
return (
<nav className="navbar bg-dark">
<h1>
<Link to={ROUTES.LANDING} data-testid="homeLink">
<Link to={Routes.LANDING} data-testid="homeLink">
<FontAwesomeIcon icon={faCode} /> DevBook
</Link>
</h1>
@ -89,6 +89,4 @@ const NavBar: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
};
/** connect HOC subscribes to the store */
const enhance = compose<FC>(connect(selectProfile), withFirebase);
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 '/'
*/
export const LANDING: string = '/';
export const SIGN_UP: string = '/signup';
export const SIGN_IN: string = '/signin';
export const DEVELOPERS: string = '/developers';
export const PROFILE: string = '/profile';
export const EDIT_PROFILE: string = '/edit-profile';
export const DASHBOARD: string = '/dashboard';
export const ADD_EXPERIENCE: string = '/add-experience';
export const ADD_EDUCATION: string = '/add-education';
export const POST: string = '/post';
export const POSTS: string = '/posts';
enum Routes {
LANDING = '/',
SIGN_UP = '/signup',
SIGN_IN = '/signin',
DEVELOPERS = '/developers',
PROFILE = '/profile',
EDIT_PROFILE = '/edit-profile',
DASHBOARD = '/dashboard',
ADD_EXPERIENCE = '/add-experience',
ADD_EDUCATION = '/add-education',
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 resetForm function to revert to initFormData
* */
const useForm = <T,>(initFormData: T) => {
const useForm = <T>(initFormData: T) => {
const [formData, setFormData] = useState<T>(initFormData);
/** update each input state value onChange */
const handleChange = (e: ChangeEvent<HTMLInputElement>): void =>
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
): void =>
setFormData({
...formData,
[e.target.name]: e.target.value,
@ -22,7 +24,11 @@ const useForm = <T,>(initFormData: T) => {
/** clean form after successful submition */
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;

View file

@ -1,11 +1,12 @@
import Experience from '../types/Experience';
import Education from '../types/Education';
import Experience from '../types/Experience';
import Links from '../types/Links';
import Repo from '../types/Repo';
/** Shorter dev interface */
export interface DevSummary {
id: string;
name: string;
displayName: string;
picture: string;
description: string;
location: string;
@ -16,45 +17,86 @@ export interface DevSummary {
* @extends DevSummary to avoid duplication
*/
interface Dev extends DevSummary {
isActive: boolean;
bio: string;
links: Object;
status: string;
company: string;
links: Links;
experiences: Experience[];
educations: Education[];
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
*/
export const dummyDev: Dev = {
id: '0',
name: 'John Doe',
isActive: true,
displayName: 'John Doe',
status: 'Developer',
company: 'Microsoft',
picture:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
description: 'Developer at Microsoft',
location: 'Seattle, WA',
skills: ['HTML', 'CSS', 'JavaScript', 'Python'],
links: {
web: '#',
website: '#',
instagram: 'http://insta.com',
facebook: '#',
linkedin: '#',
twitter: '#',
github: '#',
youtube: '#',
},
bio:
'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Blanditiis unde quae vero enim adipisci voluptas magni sapiente reprehenderit error minima.',
experiences: [
{
id: 1,
company: 'Microsoft',
from: new Date(2011, 10),
to: 'Current',
position: 'Senior Developer',
location: 'USA',
description:
'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',
location: 'USA',
from: new Date(2004, 10),
to: new Date(2010, 11),
position: 'System Admin',
@ -64,6 +106,7 @@ export const dummyDev: Dev = {
],
educations: [
{
id: 0,
school: 'University of Washington',
from: new Date(1993, 9),
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 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
*/
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">
<FormHeader
title="Add Your Education"
@ -13,13 +100,16 @@ const AddEducation: FC = () => (
icon="graduation-cap"
/>
<form className="form">
<form className="form" onSubmit={handleSubmit}>
<div className="form-group">
<input
type="text"
placeholder="* School or Bootcamp"
name="school"
value={formData.school}
onChange={handleChange}
required
autoFocus
/>
</div>
<div className="form-group">
@ -27,23 +117,47 @@ const AddEducation: FC = () => (
type="text"
placeholder="* Degree or Certificate"
name="degree"
value={formData.degree}
onChange={handleChange}
required
/>
</div>
<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 className="form-group">
<h4>From Date</h4>
<input type="date" name="from" />
<input
type="date"
name="from"
value={formData.from}
onChange={handleChange}
/>
</div>
<div className="form-group">
<h4>To Date</h4>
<input type="date" name="to" />
<input
type="date"
name="to"
value={formData.to}
onChange={handleChange}
/>
</div>
<div className="form-group">
<p>
<input type="checkbox" name="current" value="" /> Current School
<input
type="checkbox"
name="current"
checked={formData.current}
onChange={handleCheckboxesChange}
/>{' '}
Current School
</p>
</div>
<div className="form-group">
@ -52,14 +166,23 @@ const AddEducation: FC = () => (
cols={30}
rows={5}
placeholder="Program Description"
value={formData.description}
onChange={handleChange}
></textarea>
</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 className="btn btn-light my-1" to={Routes.DASHBOARD}>
Go Back
</a>
</Link>
</form>
</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 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 (
<section className="container">
<FormHeader
@ -14,27 +102,63 @@ const AddExperience: FC = () => {
icon="code-branch"
/>
<form className="form">
<form className="form" onSubmit={handleSubmit}>
<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 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 className="form-group">
<input type="text" placeholder="Location" name="location" />
<input
type="text"
placeholder="Location"
name="location"
value={formData.location}
onChange={handleChange}
/>
</div>
<div className="form-group">
<h4>From Date</h4>
<input type="date" name="from" />
<input
type="date"
name="from"
value={formData.from}
onChange={handleChange}
/>
</div>
<div className="form-group">
<h4>To Date</h4>
<input type="date" name="to" />
<input
type="date"
name="to"
value={formData.to}
onChange={handleChange}
/>
</div>
<div className="form-group">
<p>
<input type="checkbox" name="current" value="" /> Current Job
<input
type="checkbox"
name="current"
checked={formData.current}
onChange={handleCheckboxesChange}
/>{' '}
Current Job
</p>
</div>
<div className="form-group">
@ -43,15 +167,23 @@ const AddExperience: FC = () => {
cols={30}
rows={5}
placeholder="Job Description"
value={formData.description}
onChange={handleChange}
></textarea>
</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 className="btn btn-light my-1" to={Routes.DASHBOARD}>
Go Back
</a>
</Link>
</form>
</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 {
faUserCircle,
@ -7,28 +14,67 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import {faBlackTie} from '@fortawesome/free-brands-svg-icons';
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 {getTimePeriod} from '../types/TimePeriod';
import Education from '../types/Education';
interface IProps extends Dev, WithFirebaseProps<User> {}
/**
* 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 (
<section className="container">
<Header title="Dashboard" lead={`Welcome ${dev.name}`} />
<Header title="Dashboard" lead={`Welcome ${displayName}`} />
<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
</a>
<a href="add-experience.html" className="btn btn-light">
</Link>
<Link to={Routes.ADD_EXPERIENCE} className="btn btn-light">
<FontAwesomeIcon icon={faBlackTie} /> Add Experience
</a>
<a href="add-education.html" className="btn btn-light">
</Link>
<Link to={Routes.ADD_EDUCATION} className="btn btn-light">
<FontAwesomeIcon icon={faGraduationCap} /> Add Education
</a>
</Link>
</div>
<h2 className="my-2">Experience Credentials</h2>
@ -42,13 +88,18 @@ const Dashboard: FC<Dev> = () => {
</tr>
</thead>
<tbody>
{dev.experiences.map((exp: Experience, i: number) => (
<tr key={i}>
{experiences?.map((exp: Experience) => (
<tr key={exp.id}>
<td>{exp.company}</td>
<td className="hide-sm">{exp.position}</td>
<td className="hide-sm">{getTimePeriod(exp.from, exp.to)}</td>
<td>
<button className="btn btn-danger">Delete</button>
<button
className="btn btn-danger"
onClick={deleteExpEntry(exp.id, experiences)}
>
Delete
</button>
</td>
</tr>
))}
@ -66,20 +117,25 @@ const Dashboard: FC<Dev> = () => {
</tr>
</thead>
<tbody>
{dev.educations.map((edu: Education, i: number) => (
<tr key={i}>
{educations?.map((edu: Education, i: number) => (
<tr key={edu.id}>
<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>
<button className="btn btn-danger">Delete</button>
<button
className="btn btn-danger"
onClick={deleteEduEntry(edu.id, educations)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
<div className="my-2">
<button className="btn btn-danger">
<button className="btn btn-danger" onClick={deleteAccount}>
<FontAwesomeIcon icon={faUserSlash} /> Delete my Account
</button>
</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[] = [
{
id: '0',
name: 'John Doe',
displayName: 'John Doe',
picture:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
description: 'Developer at Microsoft',
@ -20,7 +20,7 @@ const Developers: FC = () => {
},
{
id: '42',
name: 'Ruidy Nemausat',
displayName: 'Ruidy Nemausat',
picture:
'https://lh3.googleusercontent.com/a-/AOh14GhncH95MWKwPR3TRKy4eVd4n6w0-fobe4dhiam2xA',
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 {
faTwitter,
@ -8,55 +14,191 @@ import {
faInstagram,
} from '@fortawesome/free-brands-svg-icons';
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.
*/
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 (
<section className="container">
<FormHeader
title="Create your profile"
title="Edit your profile"
lead="Let's get some information to make your profile stand out"
/>
<form className="form">
<form className="form" onSubmit={handleSubmit}>
<div className="form-group">
<select name="status" required>
<option value="0">* Select Professional Status</option>
<option value="Developer">Developer</option>
<option value="Junior Developer">Junior Developer</option>
<option value="Senior Developer">Senior Developer</option>
<option value="Manager">Manager</option>
<option value="Student or Learning">Student or Learning</option>
<option value="Instructor">Instructor or Teacher</option>
<option value="Intern">Intern</option>
<option value="Other">Other</option>
<select
name="status"
required
onChange={handleChange}
defaultValue={formData.status}
>
<option disabled>* Select Professional Status</option>
{Statuses.map((s: string, i: number) => (
<option value={s} key={i}>
{s}
</option>
))}
</select>
<small className="form-text">
Give us an idea of where you are at in your career
</small>
</div>
<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">
Could be your own company or one you work for
</small>
</div>
<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">
Could be your own or a company website
</small>
</div>
<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">
City & state suggested (eg. Boston, MA)
</small>
</div>
<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">
Please use comma separated values (eg. HTML,CSS,JavaScript,PHP)
</small>
@ -65,7 +207,9 @@ const EditProfile: FC = () => {
<input
type="text"
placeholder="Github Username"
name="githubusername"
name="github"
value={formData.github}
onChange={handleChange}
/>
<small className="form-text">
If you want your latest repos and a Github link, include your
@ -73,49 +217,97 @@ const EditProfile: FC = () => {
</small>
</div>
<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>
</div>
<div className="my-2">
<button type="button" className="btn btn-light">
Add Social Network Links
<button
type="button"
className="btn btn-light"
onClick={toggleSocialLinks}
>
{showLinks ? 'Hide' : 'Add'} Social Network Links
</button>
<span>Optional</span>
</div>
{showLinks && (
<>
<div className="form-group social-input">
<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 className="form-group social-input">
<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 className="form-group social-input">
<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 className="form-group social-input">
<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 className="form-group social-input">
<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>
<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
</a>
</Link>
</form>
</section>
);
};
export default EditProfile;
export default enhance(EditProfile);

View file

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

View file

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

View file

@ -56,7 +56,7 @@ const Profile: FC<Dev> = () => {
alt="Some guy"
className="round-img my-1"
/>
<h1 className="large">{dev.name}</h1>
<h1 className="large">{dev.displayName}</h1>
<p className="lead">{dev.description}</p>
<p>{dev.location}</p>
<div className="icons my-1">
@ -69,7 +69,7 @@ const Profile: FC<Dev> = () => {
</div>
<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>
<div className="line"></div>
<h2 className="text-primary">Skill Set</h2>

View file

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

View file

@ -1,12 +1,10 @@
import React, {FC, useState} from 'react';
// Routing
import {Link, Redirect} from 'react-router-dom';
import * as ROUTES from '../constants/routes';
import Routes from '../constants/routes';
// Redux
import {compose} from 'redux';
import {connect} from 'react-redux';
import {withFirebase, WithFirebaseProps} from 'react-redux-firebase';
import {selectProfile} from '../store/firebase';
import {WithFirebaseProps} from 'react-redux-firebase';
import {enhance} from '../store/firebase';
import User, {newUser} from '../models/User';
// Style
import GoogleButton from 'react-google-button';
@ -14,9 +12,10 @@ import Alert from '../components/Alert';
import Header from '../components/Header';
// Form
import useForm from '../hooks';
import Dev, {blankDev} from '../models/Dev';
// extends withFirebaseProps type to ad profile info
interface IProps extends WithFirebaseProps<User> {
interface IProps extends Dev, WithFirebaseProps<User> {
isEmpty: boolean;
isLoaded: boolean;
}
@ -31,7 +30,7 @@ interface InitFormData {
/**
* 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);
// handle form data
@ -57,16 +56,40 @@ const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
// pass the info to store into the second argument
firebase
.createUser({email, password}, newUser(name, email))
.then(() => resetForm())
.then(() => {
firebase.updateProfile(blankDev, {useSet: true, merge: true});
resetForm();
})
.catch(err => setError(err));
};
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
if (isLoaded && !isEmpty) {
return <Redirect to={ROUTES.DASHBOARD} />;
if (isLoaded && !isEmpty && isActive) {
return <Redirect to={Routes.DASHBOARD} />;
}
return (
@ -133,13 +156,11 @@ const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded}) => {
/>
</form>
<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>
</section>
);
};
/** subscribe to store and firebase */
const enhance = compose<FC<IProps>>(connect(selectProfile), withFirebase);
export default enhance(SignUp);

View file

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

View file

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

View file

@ -19,7 +19,7 @@ const rrfProps = {
createFirestoreInstance,
};
// Firestore SChema
// Firestore Schema
export interface Schema {
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 '..';
/** export firebase authentication */
export const selectAuthState = (state: RootState) => state.firebase.auth;
/** export current user 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
import {configureStore} from '@reduxjs/toolkit';
// import authReducer from './auth/';
// Firebase
import {firebaseReducer, FirebaseReducer} from 'react-redux-firebase';
import {firestoreReducer} from 'redux-firestore';
// Typing
import User from '../models/User';
import {Schema} from './firebase/config';
import Dev from '../models/Dev';
const store = configureStore({
reducer: {
// auth: authReducer,
firebase: firebaseReducer,
firestore: firestoreReducer,
},
@ -18,7 +16,7 @@ const store = configureStore({
// State type
export interface RootState {
firebase: FirebaseReducer.Reducer<User, Schema>;
firebase: FirebaseReducer.Reducer<Dev, Schema>;
}
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';
interface Education {
id: number;
school: string;
degree: string;
from: TimePeriod;
to: TimePeriod;
degree: string;
field: string;
description: string;
}

View file

@ -1,11 +1,13 @@
import TimePeriod from '../types/TimePeriod';
interface Experience {
id: number;
company: string;
from: Date;
from: TimePeriod;
to: TimePeriod;
position: string;
description: string;
location: string;
}
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';
type TimePeriod = Date | 'Current';
type TimePeriod = string | Date | 'Current';
/** format exp date to be used */
const parseDate = (date: TimePeriod): string => {
export const parseDate = (date: TimePeriod): string => {
if (date === 'Current') {
return date;
}

View file

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