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 {faCheck} from '@fortawesome/free-solid-svg-icons';
|
||||
// Typing
|
||||
import {DevSummary, getDescription} from '../models/Dev';
|
||||
import Routes from '../constants/routes';
|
||||
import {DevSummary, getDescription} from '../../models/Dev';
|
||||
import Routes from '../../constants/routes';
|
||||
|
||||
/**
|
||||
* Present a dev profile succintly. Redirect to dev profile on click.
|
||||
|
|
@ -41,4 +41,4 @@ const DevProfile: FC<DevSummary> = ({
|
|||
</div>
|
||||
);
|
||||
|
||||
export default DevProfile;
|
||||
export default React.memo(DevProfile);
|
||||
|
|
@ -3,15 +3,19 @@ import React, {FC} from 'react';
|
|||
import {compose} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {firestoreConnect} from 'react-redux-firebase';
|
||||
import {RootState} from '../store';
|
||||
import {RootState} from '../../store';
|
||||
// Style
|
||||
import Header from '../components/Header';
|
||||
import DevProfile from '../components/DevProfile';
|
||||
import {DevSummary} from '../models/Dev';
|
||||
import DevProfile from './Profile';
|
||||
import Header from '../../components/Header';
|
||||
|
||||
import {DevSummary} from '../../models/Dev';
|
||||
|
||||
import Collections from '../../constants/collections';
|
||||
|
||||
interface IProps {
|
||||
developers: DevSummary[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Developers list page
|
||||
*/
|
||||
|
|
@ -31,8 +35,8 @@ const Developers: FC<IProps> = ({developers}) => (
|
|||
);
|
||||
|
||||
export default compose<FC>(
|
||||
firestoreConnect(() => ['users']), // or { collection: 'users' }
|
||||
connect((state: RootState, props) => ({
|
||||
firestoreConnect(() => [Collections.USERS]),
|
||||
connect((state: RootState) => ({
|
||||
developers: state.firestore.ordered.users,
|
||||
})),
|
||||
)(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>
|
||||
);
|
||||
|
||||
export default Landing;
|
||||
export default React.memo(Landing);
|
||||
|
|
|
|||
|
|
@ -25,4 +25,4 @@ const NotFound: FC = () => (
|
|||
</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 {connect, useSelector} from 'react-redux';
|
||||
import {firestoreConnect, WithFirestoreProps} from 'react-redux-firebase';
|
||||
import {RootState} from '../store';
|
||||
import {selectProfile} from '../store/firebase';
|
||||
// Routing
|
||||
import {Link} from 'react-router-dom';
|
||||
import Routes from '../constants/routes';
|
||||
import {RootState} from '../../store';
|
||||
import {selectProfile} from '../../store/firebase';
|
||||
// Style
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faThumbsUp} from '@fortawesome/free-solid-svg-icons';
|
||||
import Header from '../components/Header';
|
||||
import Header from '../../components/Header';
|
||||
// Typing
|
||||
import Post from '../models/Post';
|
||||
import Collections from '../constants/collections';
|
||||
import Post from '../../models/Post';
|
||||
import Collections from '../../constants/collections';
|
||||
|
||||
import PostsForm from './Form';
|
||||
import PostsItem from './Item';
|
||||
|
||||
interface IProps extends WithFirestoreProps {
|
||||
posts: Post[];
|
||||
|
|
@ -69,45 +67,15 @@ const Posts: FC<IProps> = ({posts, firestore, firebase}) => {
|
|||
<h3>Say Something</h3>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<PostsForm
|
||||
text={text}
|
||||
handleChange={handleChange}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
|
||||
<div className="posts">
|
||||
{posts?.map((post: Post) => (
|
||||
<div className="post bg-white p-1 my-1" 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>
|
||||
<PostsItem post={post} addLike={addLike} key={post.id} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -116,7 +84,7 @@ const Posts: FC<IProps> = ({posts, firestore, firebase}) => {
|
|||
};
|
||||
|
||||
export default compose<FC>(
|
||||
firestoreConnect(() => ['posts']), // or { collection: 'users' }
|
||||
firestoreConnect(() => [Collections.POSTS]), // or { collection: 'users' }
|
||||
connect((state: RootState) => ({
|
||||
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