🏗 Folder Structure and General Refactoring (#15)

* refactor: AddEducation folder

* refactor: AddExperience folder

* refactor: separate Dashboard Buttons, Experience section

* refactor: add Dashboard Education section

* refactor: Developers  folder

* refactor: EditProfile folder

* refactor: use React.memo

* refactor: Post folder

* refactor: Posts folder

* refactor: Profile folder

* refactor: delete useless store/auth folder
This commit is contained in:
Ruidy 2020-06-02 22:49:13 +02:00 committed by GitHub
parent 04311e5abc
commit 8cf1f1f577
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1555 additions and 1291 deletions

View file

@ -1,188 +0,0 @@
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<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"
lead="Add any school, bootcamp, etc that
you have attended"
icon="graduation-cap"
/>
<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">
<input
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="field"
value={formData.field}
onChange={handleChange}
/>
</div>
<div className="form-group">
<h4>From Date</h4>
<input
type="date"
name="from"
value={formData.from}
onChange={handleChange}
/>
</div>
<div className="form-group">
<h4>To Date</h4>
<input
type="date"
name="to"
value={formData.to}
onChange={handleChange}
/>
</div>
<div className="form-group">
<p>
<input
type="checkbox"
name="current"
checked={formData.current}
onChange={handleCheckboxesChange}
/>{' '}
Current School
</p>
</div>
<div className="form-group">
<textarea
name="description"
cols={30}
rows={5}
placeholder="Program Description"
value={formData.description}
onChange={handleChange}
></textarea>
</div>
{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
</Link>
</form>
</section>
);
};
export default enhance(AddEducation);

View file

@ -0,0 +1,109 @@
import React, {FC} from 'react';
// Routing
import {Link} from 'react-router-dom';
import Routes from '../../constants/routes';
import {IEducationForm} from '.';
interface IProps {
isDisabled: boolean;
formData: IEducationForm;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
handleChange: (
e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
) => void;
handleCheckboxesChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const AddEducationForm: FC<IProps> = ({
handleSubmit,
formData,
handleChange,
handleCheckboxesChange,
isDisabled,
}) => (
<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">
<input
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="field"
value={formData.field}
onChange={handleChange}
/>
</div>
<div className="form-group">
<h4>From Date</h4>
<input
type="date"
name="from"
value={formData.from}
onChange={handleChange}
/>
</div>
<div className="form-group">
<h4>To Date</h4>
<input
type="date"
name="to"
value={formData.to}
onChange={handleChange}
/>
</div>
<div className="form-group">
<p>
<input
type="checkbox"
name="current"
checked={formData.current}
onChange={handleCheckboxesChange}
/>{' '}
Current School
</p>
</div>
<div className="form-group">
<textarea
name="description"
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"
disabled={isDisabled}
/>
<Link className="btn btn-light my-1" to={Routes.DASHBOARD}>
Go Back
</Link>
</form>
);
export default AddEducationForm;

View file

@ -0,0 +1,113 @@
import React, {FC, useState, FormEvent} from 'react';
// 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';
import AddEducationForm from './Form';
export interface IEducationForm {
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<IProps> = ({firebase, educations}) => {
const [alert, setAlert] = useState<IAlert>(formAlert);
const initFormData: IEducationForm = {
school: '',
degree: '',
field: '',
from: '',
to: '',
current: false,
description: '',
};
const {formData, handleChange, handleCheckboxesChange, resetForm} = useForm<
IEducationForm
>(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,
}: IEducationForm): 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"
lead="Add any school, bootcamp, etc. that
you have attended"
icon="graduation-cap"
/>
{alert.show && <Alert text={alert.text} color={alert.color} />}
<AddEducationForm
formData={formData}
handleSubmit={handleSubmit}
handleChange={handleChange}
handleCheckboxesChange={handleCheckboxesChange}
isDisabled={isDisabled}
/>
</section>
);
};
export default enhance(AddEducation);

View file

@ -1,189 +0,0 @@
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 Experience step to Profile
*/
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
title="Add An Experience"
lead="Add any developer/programming
positions that you have had in the past"
icon="code-branch"
/>
<form className="form" onSubmit={handleSubmit}>
<div className="form-group">
<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
value={formData.company}
onChange={handleChange}
/>
</div>
<div className="form-group">
<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"
value={formData.from}
onChange={handleChange}
/>
</div>
<div className="form-group">
<h4>To Date</h4>
<input
type="date"
name="to"
value={formData.to}
onChange={handleChange}
/>
</div>
<div className="form-group">
<p>
<input
type="checkbox"
name="current"
checked={formData.current}
onChange={handleCheckboxesChange}
/>{' '}
Current Job
</p>
</div>
<div className="form-group">
<textarea
name="description"
cols={30}
rows={5}
placeholder="Job Description"
value={formData.description}
onChange={handleChange}
></textarea>
</div>
{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
</Link>
</form>
</section>
);
};
export default enhance(AddExperience);

View file

@ -0,0 +1,108 @@
import React, {FC} from 'react';
// Routing
import {Link} from 'react-router-dom';
import Routes from '../../constants/routes';
import {IExperienceForm} from '.';
interface IProps {
isDisabled: boolean;
formData: IExperienceForm;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
handleChange: (
e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
) => void;
handleCheckboxesChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const AddExperienceForm: FC<IProps> = ({
handleSubmit,
formData,
handleChange,
handleCheckboxesChange,
isDisabled,
}) => (
<form className="form" onSubmit={handleSubmit}>
<div className="form-group">
<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
value={formData.company}
onChange={handleChange}
/>
</div>
<div className="form-group">
<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"
value={formData.from}
onChange={handleChange}
/>
</div>
<div className="form-group">
<h4>To Date</h4>
<input
type="date"
name="to"
value={formData.to}
onChange={handleChange}
/>
</div>
<div className="form-group">
<p>
<input
type="checkbox"
name="current"
checked={formData.current}
onChange={handleCheckboxesChange}
/>{' '}
Current Job
</p>
</div>
<div className="form-group">
<textarea
name="description"
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"
disabled={isDisabled}
/>
<Link className="btn btn-light my-1" to={Routes.DASHBOARD}>
Go Back
</Link>
</form>
);
export default AddExperienceForm;

View file

@ -0,0 +1,115 @@
import React, {FC, useState, FormEvent} from 'react';
// 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';
import AddExperienceForm from './Form';
export interface IExperienceForm {
position: string;
company: string;
location: string;
from: string;
to: string;
current: boolean;
description: string;
}
interface IProps extends Dev, WithFirebaseProps<User> {}
/**
* Form to add an Experience step to Profile
*/
const AddExperience: FC<IProps> = ({firebase, experiences}) => {
const [alert, setAlert] = useState<IAlert>(formAlert);
const initFormData: IExperienceForm = {
position: '',
company: '',
location: '',
from: '',
to: '',
current: false,
description: '',
};
const {formData, handleChange, handleCheckboxesChange, resetForm} = useForm<
IExperienceForm
>(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,
}: IExperienceForm): 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
title="Add An Experience"
lead="Add any developer/programming
positions that you have had in the past"
icon="code-branch"
/>
{alert.show && <Alert text={alert.text} color={alert.color} />}
<AddExperienceForm
formData={formData}
handleSubmit={handleSubmit}
handleChange={handleChange}
handleCheckboxesChange={handleCheckboxesChange}
isDisabled={isDisabled}
/>
</section>
);
};
export default enhance(AddExperience);

View file

@ -1,146 +0,0 @@
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,
faGraduationCap,
faUserSlash,
} from '@fortawesome/free-solid-svg-icons';
import {faBlackTie} from '@fortawesome/free-brands-svg-icons';
import Header from '../components/Header';
// 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<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 ${displayName}`} />
<div className="dash-buttons">
<Link to={Routes.EDIT_PROFILE} className="btn btn-light">
<FontAwesomeIcon icon={faUserCircle} /> Edit Profile
</Link>
<Link to={Routes.ADD_EXPERIENCE} className="btn btn-light">
<FontAwesomeIcon icon={faBlackTie} /> Add Experience
</Link>
<Link to={Routes.ADD_EDUCATION} className="btn btn-light">
<FontAwesomeIcon icon={faGraduationCap} /> Add Education
</Link>
</div>
<h2 className="my-2">Experience Credentials</h2>
<table className="table">
<thead>
<tr>
<th>Company</th>
<th className="hide-sm">Title</th>
<th className="hide-sm">Years</th>
<th></th>
</tr>
</thead>
<tbody>
{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"
onClick={deleteExpEntry(exp.id, experiences)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
<h2 className="my-2">Education Credentials</h2>
<table className="table">
<thead>
<tr>
<th>School</th>
<th className="hide-sm">Degree</th>
<th className="hide-sm">Years</th>
<th></th>
</tr>
</thead>
<tbody>
{educations?.map((edu: Education, i: number) => (
<tr key={edu.id}>
<td>{edu.school}</td>
<td className="hide-sm">{edu.degree}</td>
<td className="hide-sm">{getTimePeriod(edu.from, edu.to)}</td>
<td>
<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" onClick={deleteAccount}>
<FontAwesomeIcon icon={faUserSlash} /> Delete my Account
</button>
</div>
</section>
);
};
export default enhance(Dashboard);

View file

@ -0,0 +1,24 @@
import React from 'react';
// Routing
import {Link} from 'react-router-dom';
import Routes from '../../constants/routes';
// Styling
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faUserCircle, faGraduationCap} from '@fortawesome/free-solid-svg-icons';
import {faBlackTie} from '@fortawesome/free-brands-svg-icons';
const DashBoardButtons = () => (
<div className="dash-buttons">
<Link to={Routes.EDIT_PROFILE} className="btn btn-light">
<FontAwesomeIcon icon={faUserCircle} /> Edit Profile
</Link>
<Link to={Routes.ADD_EXPERIENCE} className="btn btn-light">
<FontAwesomeIcon icon={faBlackTie} /> Add Experience
</Link>
<Link to={Routes.ADD_EDUCATION} className="btn btn-light">
<FontAwesomeIcon icon={faGraduationCap} /> Add Education
</Link>
</div>
);
export default DashBoardButtons;

View file

@ -0,0 +1,49 @@
import React, {FC} from 'react';
// Typing
import Education from '../../types/Education';
import {getTimePeriod} from '../../types/TimePeriod';
interface IProps {
educations: Education[];
handleDelete: (
id: number,
entries: Education[],
) => (e: React.MouseEvent<HTMLButtonElement>) => void;
}
const DashboardEducationSection: FC<IProps> = ({educations, handleDelete}) => {
return (
<>
<h2 className="my-2">Education Credentials</h2>
<table className="table">
<thead>
<tr>
<th>School</th>
<th className="hide-sm">Degree</th>
<th className="hide-sm">Years</th>
<th></th>
</tr>
</thead>
<tbody>
{educations?.map((edu: Education, i: number) => (
<tr key={edu.id}>
<td>{edu.school}</td>
<td className="hide-sm">{edu.degree}</td>
<td className="hide-sm">{getTimePeriod(edu.from, edu.to)}</td>
<td>
<button
className="btn btn-danger"
onClick={handleDelete(edu.id, educations)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</>
);
};
export default DashboardEducationSection;

View file

@ -0,0 +1,51 @@
import React, {FC} from 'react';
// Typing
import Experience from '../../types/Experience';
import {getTimePeriod} from '../../types/TimePeriod';
interface IProps {
experiences: Experience[];
handleDelete: (
id: number,
entries: Experience[],
) => (e: React.MouseEvent<HTMLButtonElement>) => void;
}
const DashboardExperienceSection: FC<IProps> = ({
experiences,
handleDelete,
}) => {
return (
<>
<h2 className="my-2">Experience Credentials</h2>
<table className="table">
<thead>
<tr>
<th>Company</th>
<th className="hide-sm">Title</th>
<th className="hide-sm">Years</th>
<th></th>
</tr>
</thead>
<tbody>
{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"
onClick={handleDelete(exp.id, experiences)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</>
);
};
export default DashboardExperienceSection;

View file

@ -0,0 +1,84 @@
import React, {FC, MouseEvent} from 'react';
// Redux
import {WithFirebaseProps} from 'react-redux-firebase';
import {enhance} from '../../store/firebase';
// Style
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faUserSlash} from '@fortawesome/free-solid-svg-icons';
import DashBoardButtons from './Buttons';
import Header from '../../components/Header';
// Types
import Dev from '../../models/Dev';
import User from '../../models/User';
import Experience from '../../types/Experience';
import Education from '../../types/Education';
import DashboardExperienceSection from './ExperienceSection';
import DashboardEducationSection from './EducationSection';
interface IProps extends Dev, WithFirebaseProps<User> {}
/**
* Main page from which a Dev can peek and edit its own profile.
*/
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 ${displayName}`} />
<DashBoardButtons />
<DashboardExperienceSection
experiences={experiences}
handleDelete={deleteExpEntry}
/>
<DashboardEducationSection
educations={educations}
handleDelete={deleteEduEntry}
/>
<div className="my-2">
<button className="btn btn-danger" onClick={deleteAccount}>
<FontAwesomeIcon icon={faUserSlash} /> Delete my Account
</button>
</div>
</section>
);
};
export default enhance(Dashboard);

View file

@ -5,8 +5,8 @@ import {Link} from 'react-router-dom';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCheck} from '@fortawesome/free-solid-svg-icons'; import {faCheck} from '@fortawesome/free-solid-svg-icons';
// Typing // Typing
import {DevSummary, getDescription} from '../models/Dev'; import {DevSummary, getDescription} from '../../models/Dev';
import Routes from '../constants/routes'; import Routes from '../../constants/routes';
/** /**
* Present a dev profile succintly. Redirect to dev profile on click. * Present a dev profile succintly. Redirect to dev profile on click.
@ -41,4 +41,4 @@ const DevProfile: FC<DevSummary> = ({
</div> </div>
); );
export default DevProfile; export default React.memo(DevProfile);

View file

@ -3,15 +3,19 @@ import React, {FC} from 'react';
import {compose} from 'redux'; import {compose} from 'redux';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {firestoreConnect} from 'react-redux-firebase'; import {firestoreConnect} from 'react-redux-firebase';
import {RootState} from '../store'; import {RootState} from '../../store';
// Style // Style
import Header from '../components/Header'; import DevProfile from './Profile';
import DevProfile from '../components/DevProfile'; import Header from '../../components/Header';
import {DevSummary} from '../models/Dev';
import {DevSummary} from '../../models/Dev';
import Collections from '../../constants/collections';
interface IProps { interface IProps {
developers: DevSummary[]; developers: DevSummary[];
} }
/** /**
* Developers list page * Developers list page
*/ */
@ -31,8 +35,8 @@ const Developers: FC<IProps> = ({developers}) => (
); );
export default compose<FC>( export default compose<FC>(
firestoreConnect(() => ['users']), // or { collection: 'users' } firestoreConnect(() => [Collections.USERS]),
connect((state: RootState, props) => ({ connect((state: RootState) => ({
developers: state.firestore.ordered.users, developers: state.firestore.ordered.users,
})), })),
)(Developers); )(Developers);

View file

@ -1,319 +0,0 @@
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,
faFacebook,
faYoutube,
faLinkedin,
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';
import getGithubRepos from '../services/github';
// Typing
import Dev from '../models/Dev';
import User from '../models/User';
import Links, {parseLink, getGithubLink} 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<IProps> = ({
firebase,
status,
skills,
company,
links,
location,
bio,
github,
}) => {
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: 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 = async ({
status,
company,
location,
bio,
website,
instagram,
facebook,
linkedin,
twitter,
github,
youtube,
skills,
}: FormData) => {
const newLinks: Links = {
website: parseLink(website),
instagram: parseLink(instagram),
facebook: parseLink(facebook),
linkedin: parseLink(linkedin),
twitter: parseLink(twitter),
github: getGithubLink(github),
youtube: parseLink(youtube),
};
const newSkills: string[] = skills?.split(',');
const newRepos = await getGithubRepos(github);
return {
status,
company,
location,
bio,
github,
links: newLinks,
skills: newSkills,
repos: newRepos,
};
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const updatedDev = await 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) {
console.error(err);
setAlert({...alert, show: true});
}
};
const isDisabled: boolean = formData.status === '' || formData.skills === '';
const toggleSocialLinks = () => setShowLinks(!showLinks);
return (
<section className="container">
<FormHeader
title="Edit your profile"
lead="Let's get some information to make your profile stand out"
/>
<form className="form" onSubmit={handleSubmit}>
<div className="form-group">
<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"
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"
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"
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
value={formData.skills}
onChange={handleChange}
/>
<small className="form-text">
Please use comma separated values (eg. HTML,CSS,JavaScript,PHP)
</small>
</div>
<div className="form-group">
<input
type="text"
placeholder="Github Username"
name="github"
value={formData.github}
onChange={handleChange}
/>
<small className="form-text">
If you want your latest repos and a Github link, include your
username
</small>
</div>
<div className="form-group">
<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"
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"
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"
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"
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"
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"
value={formData.youtube}
onChange={handleChange}
/>
</div>
</>
)}
{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
</Link>
</form>
</section>
);
};
export default enhance(EditProfile);

View file

@ -0,0 +1,211 @@
import React, {FC} from 'react';
// Routing
import {Link} from 'react-router-dom';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faFacebook,
faInstagram,
faLinkedin,
faTwitter,
faYoutube,
} from '@fortawesome/free-brands-svg-icons';
import {IProfileForm} from '.';
import Routes from '../../constants/routes';
import Statuses from '../../constants/statuses';
interface IProps {
showLinks: boolean;
isDisabled: boolean;
formData: IProfileForm;
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
handleChange: (
e: React.ChangeEvent<
HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
>,
) => void;
toggleSocialLinks: () => void;
}
const EditProfileForm: FC<IProps> = ({
showLinks,
handleSubmit,
formData,
handleChange,
isDisabled,
toggleSocialLinks,
}) => (
<form className="form" onSubmit={handleSubmit}>
<div className="form-group">
<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"
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"
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"
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
value={formData.skills}
onChange={handleChange}
/>
<small className="form-text">
Please use comma separated values (eg. HTML,CSS,JavaScript,PHP)
</small>
</div>
<div className="form-group">
<input
type="text"
placeholder="Github Username"
name="github"
value={formData.github}
onChange={handleChange}
/>
<small className="form-text">
If you want your latest repos and a Github link, include your username
</small>
</div>
<div className="form-group">
<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"
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"
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"
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"
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"
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"
value={formData.youtube}
onChange={handleChange}
/>
</div>
</>
)}
<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
</Link>
</form>
);
export default EditProfileForm;

View file

@ -0,0 +1,147 @@
import React, {FC, useState} from 'react';
// Redux
import {enhance} from '../../store/firebase';
import {WithFirebaseProps} from 'react-redux-firebase';
// Style
import FormHeader from '../../components/FormHeader';
import Alert from '../../components/Alert';
// Form
import useForm from '../../hooks';
import getGithubRepos from '../../services/github';
// Typing
import Dev from '../../models/Dev';
import User from '../../models/User';
import Links, {parseLink, getGithubLink} from '../../types/Links';
import IAlert, {formAlert} from '../../types/Alert';
import EditProfileForm from './Form';
export interface IProfileForm {
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<IProps> = ({
firebase,
status,
skills,
company,
links,
location,
bio,
github,
}) => {
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: github ?? '',
facebook: links?.facebook ?? '',
linkedin: links?.linkedin ?? '',
instagram: links?.instagram ?? '',
twitter: links?.twitter ?? '',
youtube: links?.youtube ?? '',
};
const {formData, handleChange} = useForm<IProfileForm>(initFormData);
/** construct profile object from formData */
const makeProfile = async ({
status,
company,
location,
bio,
website,
instagram,
facebook,
linkedin,
twitter,
github,
youtube,
skills,
}: IProfileForm) => {
const newLinks: Links = {
website: parseLink(website),
instagram: parseLink(instagram),
facebook: parseLink(facebook),
linkedin: parseLink(linkedin),
twitter: parseLink(twitter),
github: getGithubLink(github),
youtube: parseLink(youtube),
};
const newSkills: string[] = skills?.split(',');
const newRepos = await getGithubRepos(github);
return {
status,
company,
location,
bio,
github,
links: newLinks,
skills: newSkills,
repos: newRepos,
};
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const updatedDev = await 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) {
console.error(err);
setAlert({...alert, show: true});
}
};
const isDisabled: boolean = formData.status === '' || formData.skills === '';
const toggleSocialLinks = () => setShowLinks(!showLinks);
return (
<section className="container">
<FormHeader
title="Edit your profile"
lead="Let's get some information to make your profile stand out"
/>
{alert.show && <Alert text={alert.text} color={alert.color} />}
<EditProfileForm
formData={formData}
handleChange={handleChange}
handleSubmit={handleSubmit}
isDisabled={isDisabled}
toggleSocialLinks={toggleSocialLinks}
showLinks={showLinks}
/>
</section>
);
};
export default enhance(EditProfile);

View file

@ -28,4 +28,4 @@ const Landing: FC = () => (
</section> </section>
); );
export default Landing; export default React.memo(Landing);

View file

@ -25,4 +25,4 @@ const NotFound: FC = () => (
</section> </section>
); );
export default NotFound; export default React.memo(NotFound);

View file

@ -1,115 +0,0 @@
import React, {FC} from 'react';
// Routing
import {useParams, Link} from 'react-router-dom';
import Routes from '../constants/routes';
// Redux
import {compose} from '@reduxjs/toolkit';
import {connect, useSelector} from 'react-redux';
import {firestoreConnect, WithFirestoreProps} from 'react-redux-firebase';
import {RootState} from '../store';
import {selectProfile} from '../store/firebase';
import Collections from '../constants/collections';
// Typing
import Post from '../models/Post';
import Comment from '../types/Comment';
// Form
import useForm from '../hooks';
interface IProps extends WithFirestoreProps {
post: Post;
}
/**
* Display a Post and the related comments. Shows a form to add a comment.
*/
const PostPage: FC<IProps> = ({post, firestore}) => {
const {avatarUrl, displayName} = useSelector(selectProfile);
const newComment: Comment = {
name: displayName ?? 'error',
avatarUrl: avatarUrl ?? 'error',
text: '',
};
const {formData, handleChange, resetForm} = useForm(newComment);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
firestore
.update(`${Collections.POSTS}/${post.id}`, {
comments: [...post.comments, formData],
})
.then(() => resetForm())
.catch(err => console.error(err));
};
return (
<section className="container">
<Link to={Routes.POSTS} className="btn btn-light">
Back To Posts
</Link>
<div className="post bg-white p-1 my-1">
<div>
<Link to={`${Routes.PROFILE}/${post.userID}`}>
<img className="round-img" src={post.avatarUrl} alt={post.name} />
<h4>{post.name}</h4>
</Link>
</div>
<div>
<p className="my-1">{post.text}</p>
</div>
</div>
<div className="post-form">
<div className="post-form-header bg-primary">
<h3>Leave A Comment</h3>
</div>
<form className="form my-1" onSubmit={handleSubmit}>
<textarea
name="text"
cols={30}
rows={5}
placeholder="Comment on this post"
value={formData.text}
onChange={handleChange}
></textarea>
<input type="submit" className="btn btn-dark my-1" value="Submit" />
</form>
</div>
<div className="posts">
{post.comments?.map((c: Comment, i: number) => (
<div className="post bg-white p-1 my-1" key={i}>
<div>
<a href="profile.html">
<img className="round-img" src={c.avatarUrl} alt={c.name} />
<h4>{c.name}</h4>
</a>
</div>
<div>
<p className="my-1">{c.text}</p>
</div>
</div>
))}
</div>
</section>
);
};
/**
* Container to fetch id params from thr URI and pass it to Profile page
*/
const PostPageContainer: FC = () => {
const {id} = useParams();
const Component = compose<FC>(
firestoreConnect(() => [`${Collections.POSTS}/${id}`]),
connect(({firestore: {data}}: RootState) => ({
post: data.posts && {...data.posts[id], id},
})),
)(PostPage);
return <Component />;
};
export default PostPageContainer;

View file

@ -0,0 +1,26 @@
import React, {FC} from 'react';
import Comment from '../../types/Comment';
interface IProps {
comments: Comment[];
}
const PostComments: FC<IProps> = ({comments}) => (
<div className="posts">
{comments?.map(({name, avatarUrl, text}: Comment, i: number) => (
<div className="post bg-white p-1 my-1" key={i}>
<div>
<a href="profile.html">
<img className="round-img" src={avatarUrl} alt={name} />
<h4>{name}</h4>
</a>
</div>
<div>
<p className="my-1">{text}</p>
</div>
</div>
))}
</div>
);
export default PostComments;

View file

@ -0,0 +1,24 @@
import React, {FC} from 'react';
// Routing
import {Link} from 'react-router-dom';
import Routes from '../../constants/routes';
import Post from '../../models/Post';
const PostDisplay: FC<{post: Post}> = ({
post: {name, userID, avatarUrl, text},
}) => (
<div className="post bg-white p-1 my-1">
<div>
<Link to={`${Routes.PROFILE}/${userID}`}>
<img className="round-img" src={avatarUrl} alt={name} />
<h4>{name}</h4>
</Link>
</div>
<div>
<p className="my-1">{text}</p>
</div>
</div>
);
export default PostDisplay;

28
src/pages/Post/Form.tsx Normal file
View file

@ -0,0 +1,28 @@
import React, {FC} from 'react';
interface IProps {
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
handleChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
text: string;
}
const PostForm: FC<IProps> = ({text, handleChange, handleSubmit}) => (
<div className="post-form">
<div className="post-form-header bg-primary">
<h3>Leave A Comment</h3>
</div>
<form className="form my-1" onSubmit={handleSubmit}>
<textarea
name="text"
cols={30}
rows={5}
placeholder="Comment on this post"
value={text}
onChange={handleChange}
></textarea>
<input type="submit" className="btn btn-dark my-1" value="Submit" />
</form>
</div>
);
export default PostForm;

84
src/pages/Post/index.tsx Normal file
View file

@ -0,0 +1,84 @@
import React, {FC} from 'react';
// Routing
import {useParams, Link} from 'react-router-dom';
import Routes from '../../constants/routes';
// Redux
import {compose} from '@reduxjs/toolkit';
import {connect, useSelector} from 'react-redux';
import {firestoreConnect, WithFirestoreProps} from 'react-redux-firebase';
import {RootState} from '../../store';
import {selectProfile} from '../../store/firebase';
import Collections from '../../constants/collections';
// Typing
import Post from '../../models/Post';
import Comment from '../../types/Comment';
// Form
import useForm from '../../hooks';
import PostForm from './Form';
import PostComments from './Comments';
import PostDisplay from './Display';
interface IProps extends WithFirestoreProps {
post: Post;
}
/**
* Display a Post and the related comments. Shows a form to add a comment.
*/
const PostComponent: FC<IProps> = ({post, firestore}) => {
const {avatarUrl, displayName} = useSelector(selectProfile);
const newComment: Comment = {
name: displayName ?? 'error',
avatarUrl: avatarUrl ?? 'error',
text: '',
};
const {formData, handleChange, resetForm} = useForm(newComment);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
firestore
.update(`${Collections.POSTS}/${post.id}`, {
comments: [...post.comments, formData],
})
.then(() => resetForm())
.catch(err => console.error(err));
};
return (
<section className="container">
<Link to={Routes.POSTS} className="btn btn-light">
Back To Posts
</Link>
<PostDisplay post={post} />
<PostForm
text={formData.text}
handleChange={handleChange}
handleSubmit={handleSubmit}
/>
<PostComments comments={post.comments} />
</section>
);
};
/**
* Container to fetch id params from thr URI and pass it to Profile page
*/
const PostPage: FC = () => {
const {id} = useParams();
const Component = compose<FC>(
firestoreConnect(() => [`${Collections.POSTS}/${id}`]),
connect(({firestore: {data}}: RootState) => ({
post: data.posts && {...data.posts[id], id},
})),
)(PostComponent);
return <Component />;
};
export default PostPage;

22
src/pages/Posts/Form.tsx Normal file
View file

@ -0,0 +1,22 @@
import React, {FC} from 'react';
interface IProps {
text: string;
handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
handleChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
}
const PostsForm: FC<IProps> = ({text, handleSubmit, handleChange}) => (
<form className="form my-1" onSubmit={handleSubmit}>
<textarea
cols={30}
rows={5}
placeholder="Create a post"
value={text}
onChange={handleChange}
/>
<input type="submit" value="Submit" className="btn btn-dark my-1" />
</form>
);
export default PostsForm;

41
src/pages/Posts/Item.tsx Normal file
View file

@ -0,0 +1,41 @@
import React, {FC} from 'react';
// Routing
import {Link} from 'react-router-dom';
import Routes from '../../constants/routes';
// Styling
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faThumbsUp} from '@fortawesome/free-solid-svg-icons';
import Post from '../../models/Post';
interface IProps {
post: Post;
addLike: (
postID: string,
) => (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}
const PostsItem: FC<IProps> = ({post, addLike}) => (
<div className="post bg-white p-1 my-1">
<div>
<Link to={`${Routes.PROFILE}/${post.userID}`}>
<img src={post.avatarUrl} alt={post.name} className="round-img" />
<h4>{post.name}</h4>
</Link>
</div>
<div>
<p className="my-1">{post.text}</p>
<button className="btn btn-light" onClick={addLike(post.id)}>
<FontAwesomeIcon icon={faThumbsUp} /> {post.likes?.length}
</button>
{/* <button className="btn btn-light" onClick={removeLike}>
<FontAwesomeIcon icon={faThumbsDown} />
</button> */}
<Link to={`${Routes.POST}/${post.id}`} className="btn btn-primary">
Discussion
</Link>
</div>
</div>
);
export default React.memo(PostsItem);

View file

@ -3,18 +3,16 @@ import React, {FC, useState} from 'react';
import {compose} from '@reduxjs/toolkit'; import {compose} from '@reduxjs/toolkit';
import {connect, useSelector} from 'react-redux'; import {connect, useSelector} from 'react-redux';
import {firestoreConnect, WithFirestoreProps} from 'react-redux-firebase'; import {firestoreConnect, WithFirestoreProps} from 'react-redux-firebase';
import {RootState} from '../store'; import {RootState} from '../../store';
import {selectProfile} from '../store/firebase'; import {selectProfile} from '../../store/firebase';
// Routing
import {Link} from 'react-router-dom';
import Routes from '../constants/routes';
// Style // Style
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import Header from '../../components/Header';
import {faThumbsUp} from '@fortawesome/free-solid-svg-icons';
import Header from '../components/Header';
// Typing // Typing
import Post from '../models/Post'; import Post from '../../models/Post';
import Collections from '../constants/collections'; import Collections from '../../constants/collections';
import PostsForm from './Form';
import PostsItem from './Item';
interface IProps extends WithFirestoreProps { interface IProps extends WithFirestoreProps {
posts: Post[]; posts: Post[];
@ -69,45 +67,15 @@ const Posts: FC<IProps> = ({posts, firestore, firebase}) => {
<h3>Say Something</h3> <h3>Say Something</h3>
</div> </div>
<form className="form my-1" onSubmit={handleSubmit}> <PostsForm
<textarea text={text}
cols={30} handleChange={handleChange}
rows={5} handleSubmit={handleSubmit}
placeholder="Create a post" />
value={text}
onChange={handleChange}
/>
<input type="submit" value="Submit" className="btn btn-dark my-1" />
</form>
<div className="posts"> <div className="posts">
{posts?.map((post: Post) => ( {posts?.map((post: Post) => (
<div className="post bg-white p-1 my-1" key={post.id}> <PostsItem post={post} addLike={addLike} key={post.id} />
<div>
<Link to={`${Routes.PROFILE}/${post.userID}`}>
<img
src={post.avatarUrl}
alt={post.name}
className="round-img"
/>
<h4>{post.name}</h4>
</Link>
</div>
<div>
<p className="my-1">{post.text}</p>
<button className="btn btn-light" onClick={addLike(post.id)}>
<FontAwesomeIcon icon={faThumbsUp} /> {post.likes?.length}
</button>
{/* <button className="btn btn-light" onClick={removeLike}>
<FontAwesomeIcon icon={faThumbsDown} />
</button> */}
<Link
to={`${Routes.POST}/${post.id}`}
className="btn btn-primary"
>
Discussion
</Link>
</div>
</div>
))} ))}
</div> </div>
</div> </div>
@ -116,7 +84,7 @@ const Posts: FC<IProps> = ({posts, firestore, firebase}) => {
}; };
export default compose<FC>( export default compose<FC>(
firestoreConnect(() => ['posts']), // or { collection: 'users' } firestoreConnect(() => [Collections.POSTS]), // or { collection: 'users' }
connect((state: RootState) => ({ connect((state: RootState) => ({
posts: state.firestore.ordered.posts, posts: state.firestore.ordered.posts,
})), })),

View file

@ -1,239 +0,0 @@
import React, {FC} from 'react';
// Redux
import {compose} from '@reduxjs/toolkit';
import {firestoreConnect} from 'react-redux-firebase';
import {connect} from 'react-redux';
import {RootState} from '../store';
// Routing
import {Link, useParams} from 'react-router-dom';
import Routes from '../constants/routes';
import NotFound from './NotFound';
// Style
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faGithub,
faFacebook,
faInstagram,
faLinkedin,
faTwitter,
faYoutube,
} from '@fortawesome/free-brands-svg-icons';
import {
faGlobe,
IconDefinition,
faCheck,
faStar,
faEye,
faCodeBranch,
} from '@fortawesome/free-solid-svg-icons';
// Typing
import IDev, {getDescription} from '../models/Dev';
import Experience from '../types/Experience';
import {getTimePeriod} from '../types/TimePeriod';
import Education from '../types/Education';
import Repo from '../types/Repo';
interface IProps {
dev: IDev;
}
/**
* Dev personal profile as seen by other people.
*/
const Profile: FC<IProps> = ({dev}) => {
// display 404 page if dev is null
if (dev === null) {
return <NotFound />;
}
/** return the icon corresponding to the social name */
const renderSocialIcon = (name: string): IconDefinition => {
switch (name) {
case 'facebook':
return faFacebook;
case 'github':
return faGithub;
case 'instagram':
return faInstagram;
case 'linkedin':
return faLinkedin;
case 'twitter':
return faTwitter;
case 'youtube':
return faYoutube;
default:
return faGlobe;
}
};
return dev === undefined ? (
<div>Loading ... </div>
) : (
<section className="container">
<Link to={Routes.DEVELOPERS} className="btn">
Back to profiles
</Link>
<div className="profile-grid my-1">
<div className="profile-top bg-primary p-2">
<img
src={dev.avatarUrl}
alt={dev.displayName}
className="round-img my-1"
/>
<h1 className="large">{dev.displayName}</h1>
<p className="lead">{getDescription(dev.status, dev.company)}</p>
<p>{dev.location}</p>
<div className="icons my-1">
{Object.entries(dev.links)
.sort()
.map(([icon, webAddress], i: number) => (
<a
href={webAddress}
key={i}
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={renderSocialIcon(icon)} size="2x" />
</a>
))}
</div>
</div>
<div className="profile-about bg-light p-2">
<h2 className="text-primary">{`${dev.displayName}'s Bio`}</h2>
<p>
{dev.bio.length === 0
? 'Add a short bio to present yourself!'
: dev.bio}
</p>
<div className="line"></div>
<h2 className="text-primary">Skill Set</h2>
<div className="skills">
{dev.skills.length === 0
? 'Let us know about your skills!'
: dev.skills?.map((s: string, i: number) => (
<div className="p-1" key={i}>
<FontAwesomeIcon icon={faCheck} /> {s}
</div>
))}
</div>
</div>
<div className="profile-exp bg-white p-2">
<h2 className="text-primary">Experiences</h2>
{dev.experiences.length === 0 ? (
<div>
<img
src={require('../static/img/404.jpg')}
alt="no experiences"
/>
</div>
) : (
dev.experiences.map((exp: Experience, i: number) => (
<div key={i}>
<h3>{exp.company}</h3>
<p>{getTimePeriod(exp.from, exp.to)}</p>
<p>
<strong>Position: </strong>
{exp.position}
</p>
<p>
<strong>Description: </strong>
{exp.description}
</p>
</div>
))
)}
</div>
<div className="profile-edu bg-white p-2">
<h2 className="text-primary">Education</h2>
{dev.educations.length === 0 ? (
<div>
<img src={require('../static/img/404.jpg')} alt="no educations" />
</div>
) : (
dev.educations.map((edu: Education, i: number) => (
<div key={i}>
<h3>{edu.school}</h3>
<p>{getTimePeriod(edu.from, edu.to)}</p>
<p>
<strong>Degree: </strong>
{edu.degree}
</p>
<p>
<strong>Field: </strong>
{edu.field}
</p>
<p>
<strong>Description: </strong>
{edu.description}
</p>
</div>
))
)}
</div>
<div className="profile-github">
<h2 className="text-primary my-1">
<FontAwesomeIcon icon={faGithub} /> GitHub Repos
</h2>
{dev.repos?.length === 0 ? (
<div>
<img
src={require('../static/img/404.jpg')}
alt="no repositories"
/>
</div>
) : (
dev.repos.map((r: Repo, i: number) => (
<div className="repo bg-white my-1 p-1" key={i}>
<div>
<h4>
<a href={r.url} target="_blank" rel="noopener noreferrer">
{r.name}
</a>
</h4>
<p>{r.description}</p>
</div>
<div>
<ul>
<li className="badge badge-primary">
<FontAwesomeIcon icon={faStar} /> Stars: {r.stars}
</li>
<li className="badge badge-dark">
<FontAwesomeIcon icon={faEye} /> Watchers: {r.watchers}
</li>
<li className="badge badge-light">
<FontAwesomeIcon icon={faCodeBranch} /> Forks: {r.forks}
</li>
</ul>
</div>
</div>
))
)}
</div>
</div>
</section>
);
};
/**
* Container to fetch id params from thr URI and pass it to Profile page
*/
const ProfileContainer: FC = () => {
const {id} = useParams();
const Component = compose<FC>(
firestoreConnect(() => [`users/${id}`]),
connect(({firestore: {data}}: RootState) => ({
dev: data.users && data.users[id],
})),
)(Profile);
return <Component />;
};
export default ProfileContainer;

View file

@ -0,0 +1,26 @@
import React, {FC} from 'react';
// Styling
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCheck} from '@fortawesome/free-solid-svg-icons';
import IDev from '../../models/Dev';
const ProfileAbout: FC<IDev> = ({displayName, bio, skills}) => (
<div className="profile-about bg-light p-2">
<h2 className="text-primary">{`${displayName}'s Bio`}</h2>
<p>{bio.length === 0 ? 'Add a short bio to present yourself!' : bio}</p>
<div className="line"></div>
<h2 className="text-primary">Skill Set</h2>
<div className="skills">
{skills.length === 0
? 'Let us know about your skills!'
: skills?.map((s: string, i: number) => (
<div className="p-1" key={i}>
<FontAwesomeIcon icon={faCheck} /> {s}
</div>
))}
</div>
</div>
);
export default React.memo(ProfileAbout);

View file

@ -0,0 +1,38 @@
import React, {FC} from 'react';
import Education from '../../types/Education';
import {getTimePeriod} from '../../types/TimePeriod';
import educationPicture from '../../static/img/education.svg';
const ProfileEducation: FC<{educations: Education[]}> = ({educations}) => (
<div className="profile-edu bg-white p-2">
<h2 className="text-primary">Education</h2>
{educations.length === 0 ? (
<div>
<img src={educationPicture} alt="no educations" />
</div>
) : (
educations.map((edu: Education, i: number) => (
<div key={i}>
<h3>{edu.school}</h3>
<p>{getTimePeriod(edu.from, edu.to)}</p>
<p>
<strong>Degree: </strong>
{edu.degree}
</p>
<p>
<strong>Field: </strong>
{edu.field}
</p>
<p>
<strong>Description: </strong>
{edu.description}
</p>
</div>
))
)}
</div>
);
export default React.memo(ProfileEducation);

View file

@ -0,0 +1,34 @@
import React, {FC} from 'react';
import Experience from '../../types/Experience';
import {getTimePeriod} from '../../types/TimePeriod';
import expPicture from '../../static/img/experience.svg';
const ProfileExperience: FC<{experiences: Experience[]}> = ({experiences}) => (
<div className="profile-exp bg-white p-2">
<h2 className="text-primary">Experiences</h2>
{experiences.length === 0 ? (
<div>
<img src={expPicture} alt="no experiences" />
</div>
) : (
experiences.map((exp: Experience, i: number) => (
<div key={i}>
<h3>{exp.company}</h3>
<p>{getTimePeriod(exp.from, exp.to)}</p>
<p>
<strong>Position: </strong>
{exp.position}
</p>
<p>
<strong>Description: </strong>
{exp.description}
</p>
</div>
))
)}
</div>
);
export default ProfileExperience;

View file

@ -0,0 +1,50 @@
import React, {FC} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faGithub} from '@fortawesome/free-brands-svg-icons';
import {faStar, faEye, faCodeBranch} from '@fortawesome/free-solid-svg-icons';
import Repo from '../../types/Repo';
import githubPicture from '../../static/img/github.svg';
const ProfileGithub: FC<{repos: Repo[]}> = ({repos}) => (
<div className="profile-github">
<h2 className="text-primary my-1">
<FontAwesomeIcon icon={faGithub} /> GitHub Repos
</h2>
{repos?.length === 0 ? (
<div>
<img src={githubPicture} alt="no repositories" />
</div>
) : (
repos.map((r: Repo, i: number) => (
<div className="repo bg-white my-1 p-1" key={i}>
<div>
<h4>
<a href={r.url} target="_blank" rel="noopener noreferrer">
{r.name}
</a>
</h4>
<p>{r.description}</p>
</div>
<div>
<ul>
<li className="badge badge-primary">
<FontAwesomeIcon icon={faStar} /> Stars: {r.stars}
</li>
<li className="badge badge-dark">
<FontAwesomeIcon icon={faEye} /> Watchers: {r.watchers}
</li>
<li className="badge badge-light">
<FontAwesomeIcon icon={faCodeBranch} /> Forks: {r.forks}
</li>
</ul>
</div>
</div>
))
)}
</div>
);
export default React.memo(ProfileGithub);

68
src/pages/Profile/Top.tsx Normal file
View file

@ -0,0 +1,68 @@
import React, {FC} from 'react';
// Styling
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {IconDefinition} from '@fortawesome/fontawesome-svg-core';
import {
faFacebook,
faGithub,
faInstagram,
faLinkedin,
faTwitter,
faYoutube,
} from '@fortawesome/free-brands-svg-icons';
import {faGlobe} from '@fortawesome/free-solid-svg-icons';
import IDev, {getDescription} from '../../models/Dev';
const ProfileTop: FC<IDev> = ({
avatarUrl,
displayName,
status,
company,
links,
location,
}) => {
/** return the icon corresponding to the social name */
const renderSocialIcon = (name: string): IconDefinition => {
switch (name) {
case 'facebook':
return faFacebook;
case 'github':
return faGithub;
case 'instagram':
return faInstagram;
case 'linkedin':
return faLinkedin;
case 'twitter':
return faTwitter;
case 'youtube':
return faYoutube;
default:
return faGlobe;
}
};
return (
<div className="profile-top bg-primary p-2">
<img src={avatarUrl} alt={displayName} className="round-img my-1" />
<h1 className="large">{displayName}</h1>
<p className="lead">{getDescription(status, company)}</p>
<p>{location}</p>
<div className="icons my-1">
{Object.entries(links)
.sort()
.map(([icon, webAddress], i: number) => (
<a
href={webAddress}
key={i}
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={renderSocialIcon(icon)} size="2x" />
</a>
))}
</div>
</div>
);
};
export default React.memo(ProfileTop);

View file

@ -0,0 +1,69 @@
import React, {FC} from 'react';
// Redux
import {compose} from '@reduxjs/toolkit';
import {firestoreConnect} from 'react-redux-firebase';
import {connect} from 'react-redux';
import {RootState} from '../../store';
// Routing
import {Link, useParams} from 'react-router-dom';
import Routes from '../../constants/routes';
import NotFound from '../NotFound';
// Typing
import IDev from '../../models/Dev';
import ProfileTop from './Top';
import ProfileAbout from './About';
import ProfileExperience from './Experience';
import ProfileEducation from './Education';
import ProfileGithub from './Github';
import Collections from '../../constants/collections';
interface IProps {
dev: IDev;
}
/**
* Dev personal profile as seen by other people.
*/
const ProfileComponent: FC<IProps> = ({dev}) => {
// display 404 page if dev is null
if (dev === null) {
return <NotFound />;
}
return dev === undefined ? (
<div>Loading ... </div>
) : (
<section className="container">
<Link to={Routes.DEVELOPERS} className="btn">
Back to profiles
</Link>
<div className="profile-grid my-1">
<ProfileTop {...dev} />
<ProfileAbout {...dev} />
<ProfileExperience experiences={dev.experiences} />
<ProfileEducation educations={dev.educations} />
<ProfileGithub repos={dev.repos} />
</div>
</section>
);
};
/**
* Container to fetch id params from thr URI and pass it to Profile page
*/
const Profile: FC = () => {
const {id} = useParams();
const Component = compose<FC>(
firestoreConnect(() => [`${Collections.USERS}/${id}`]),
connect(({firestore: {data}}: RootState) => ({
dev: data.users && data.users[id],
})),
)(ProfileComponent);
return <Component />;
};
export default Profile;

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -1,36 +0,0 @@
// Redux
import {createSlice} from '@reduxjs/toolkit';
// Typing
import User from '../../models/User';
interface SliceState {
isAuthenticated: boolean;
loading: boolean;
user: User | null;
error: string | null;
}
const initialState: SliceState = {
isAuthenticated: false,
loading: true,
user: null,
error: null,
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {},
});
// export actions
export const {} = authSlice.actions;
// export selectors
// export const selectAuthState = (state: RootState) => {
// const {isAuthenticated, loading} = state.auth;
// return {isAuthenticated, loading};
// };
// export reducer
export default authSlice.reducer;