mirror of
https://github.com/rjNemo/devbook_ts
synced 2026-06-06 02:36:39 +00:00
🏗 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:
parent
04311e5abc
commit
8cf1f1f577
37 changed files with 1555 additions and 1291 deletions
|
|
@ -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);
|
|
||||||
109
src/pages/AddEducation/Form.tsx
Normal file
109
src/pages/AddEducation/Form.tsx
Normal 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;
|
||||||
113
src/pages/AddEducation/index.tsx
Normal file
113
src/pages/AddEducation/index.tsx
Normal 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);
|
||||||
|
|
@ -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);
|
|
||||||
108
src/pages/AddExperience/Form.tsx
Normal file
108
src/pages/AddExperience/Form.tsx
Normal 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;
|
||||||
115
src/pages/AddExperience/index.tsx
Normal file
115
src/pages/AddExperience/index.tsx
Normal 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);
|
||||||
|
|
@ -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);
|
|
||||||
24
src/pages/Dashboard/Buttons.tsx
Normal file
24
src/pages/Dashboard/Buttons.tsx
Normal 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;
|
||||||
49
src/pages/Dashboard/EducationSection.tsx
Normal file
49
src/pages/Dashboard/EducationSection.tsx
Normal 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;
|
||||||
51
src/pages/Dashboard/ExperienceSection.tsx
Normal file
51
src/pages/Dashboard/ExperienceSection.tsx
Normal 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;
|
||||||
84
src/pages/Dashboard/index.tsx
Normal file
84
src/pages/Dashboard/index.tsx
Normal 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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
|
||||||
211
src/pages/EditProfile/Form.tsx
Normal file
211
src/pages/EditProfile/Form.tsx
Normal 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;
|
||||||
147
src/pages/EditProfile/index.tsx
Normal file
147
src/pages/EditProfile/index.tsx
Normal 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);
|
||||||
|
|
@ -28,4 +28,4 @@ const Landing: FC = () => (
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default Landing;
|
export default React.memo(Landing);
|
||||||
|
|
|
||||||
|
|
@ -25,4 +25,4 @@ const NotFound: FC = () => (
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default NotFound;
|
export default React.memo(NotFound);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
26
src/pages/Post/Comments.tsx
Normal file
26
src/pages/Post/Comments.tsx
Normal 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;
|
||||||
24
src/pages/Post/Display.tsx
Normal file
24
src/pages/Post/Display.tsx
Normal 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
28
src/pages/Post/Form.tsx
Normal 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
84
src/pages/Post/index.tsx
Normal 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
22
src/pages/Posts/Form.tsx
Normal 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
41
src/pages/Posts/Item.tsx
Normal 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);
|
||||||
|
|
@ -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,
|
||||||
})),
|
})),
|
||||||
|
|
@ -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;
|
|
||||||
26
src/pages/Profile/About.tsx
Normal file
26
src/pages/Profile/About.tsx
Normal 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);
|
||||||
38
src/pages/Profile/Education.tsx
Normal file
38
src/pages/Profile/Education.tsx
Normal 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);
|
||||||
34
src/pages/Profile/Experience.tsx
Normal file
34
src/pages/Profile/Experience.tsx
Normal 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;
|
||||||
50
src/pages/Profile/Github.tsx
Normal file
50
src/pages/Profile/Github.tsx
Normal 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
68
src/pages/Profile/Top.tsx
Normal 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);
|
||||||
69
src/pages/Profile/index.tsx
Normal file
69
src/pages/Profile/index.tsx
Normal 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;
|
||||||
1
src/static/img/education.svg
Normal file
1
src/static/img/education.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.4 KiB |
1
src/static/img/experience.svg
Normal file
1
src/static/img/experience.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.9 KiB |
1
src/static/img/github.svg
Normal file
1
src/static/img/github.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -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;
|
|
||||||
Loading…
Reference in a new issue