mirror of
https://github.com/rjNemo/devbook_ts
synced 2026-06-06 02:36:39 +00:00
📑Profile list (#11)
* edit github workflows * document Altert type * add firestore reducer * connect developers profile to store * switch picture field to avatarUrl * handle document uid * add param to profile route * use id parameter for profile * redirect to notfound page if dev is null * wait for profile to be loaded before displaying profile * add Dev class, IDev interface, remove blankDev and getDescription method * profile-top * format social links * profile-about * profile description * add placeholders to profile * alt tag on placeholders * deploy.yml
This commit is contained in:
parent
75c9888493
commit
309ee76a32
11 changed files with 313 additions and 163 deletions
9
.github/workflows/deploy.yml
vendored
9
.github/workflows/deploy.yml
vendored
|
|
@ -29,6 +29,15 @@ jobs:
|
|||
name: Release
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REACT_APP_STORAGE_BUCKET: ${{ secrets.REACT_APP_STORAGE_BUCKET }}
|
||||
REACT_APP_PROJECT_ID: ${{ secrets.REACT_APP_PROJECT_ID }}
|
||||
REACT_APP_MSG_SENDER_ID: ${{ secrets.REACT_APP_MSG_SENDER_ID }}
|
||||
REACT_APP_MEASUREMENT_ID: ${{ secrets.REACT_APP_MEASUREMENT_ID }}
|
||||
REACT_APP_DB_URL: ${{ secrets.REACT_APP_DB_URL }}
|
||||
REACT_APP_AUTH_DOMAIN: ${{ secrets.REACT_APP_AUTH_DOMAIN }}
|
||||
REACT_APP_APP_ID: ${{ secrets.REACT_APP_APP_ID }}
|
||||
REACT_APP_API_KEY: ${{ secrets.REACT_APP_API_KEY }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import React, {FC} from 'react';
|
||||
// Routing
|
||||
import {Link} from 'react-router-dom';
|
||||
// Style
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faCheck} from '@fortawesome/free-solid-svg-icons';
|
||||
import {DevSummary} from '../models/Dev';
|
||||
// Typing
|
||||
import {DevSummary, getDescription} from '../models/Dev';
|
||||
import Routes from '../constants/routes';
|
||||
|
||||
/**
|
||||
* Present a dev profile succintly. Redirect to dev profile on click.
|
||||
|
|
@ -10,23 +15,24 @@ import {DevSummary} from '../models/Dev';
|
|||
const DevProfile: FC<DevSummary> = ({
|
||||
id,
|
||||
displayName,
|
||||
picture,
|
||||
description,
|
||||
avatarUrl,
|
||||
status,
|
||||
company,
|
||||
location,
|
||||
skills,
|
||||
}) => (
|
||||
<div className="profile bg-light">
|
||||
<img src={picture} alt={displayName} className="round-img" />
|
||||
<img src={avatarUrl} alt={displayName} className="round-img" />
|
||||
<div>
|
||||
<h2>{displayName}</h2>
|
||||
<p>{description}</p>
|
||||
<p>{getDescription(status, company)}</p>
|
||||
<p>{location}</p>
|
||||
<a href="profile.html" className="btn btn-primary">
|
||||
<Link to={`${Routes.PROFILE}/${id}`} className="btn btn-primary">
|
||||
View Profile
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<ul>
|
||||
{skills.map((s, i) => (
|
||||
{skills?.map((s, i) => (
|
||||
<li className="text-primary" key={i}>
|
||||
<FontAwesomeIcon icon={faCheck} /> {s}
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ import Repo from '../types/Repo';
|
|||
|
||||
/** Shorter dev interface */
|
||||
export interface DevSummary {
|
||||
id: string;
|
||||
id?: string;
|
||||
displayName: string;
|
||||
picture: string;
|
||||
avatarUrl: string;
|
||||
description: string;
|
||||
status: string;
|
||||
company: string;
|
||||
location: string;
|
||||
skills: string[];
|
||||
}
|
||||
|
|
@ -16,33 +18,40 @@ export interface DevSummary {
|
|||
/** Full developer profile information.
|
||||
* @extends DevSummary to avoid duplication
|
||||
*/
|
||||
interface Dev extends DevSummary {
|
||||
interface IDev extends DevSummary {
|
||||
isActive: boolean;
|
||||
bio: string;
|
||||
status: string;
|
||||
company: string;
|
||||
github: string;
|
||||
links: Links;
|
||||
experiences: Experience[];
|
||||
educations: Education[];
|
||||
repos: Repo[];
|
||||
}
|
||||
|
||||
/** create profile tagline */
|
||||
export const getDescription = (status: string, company: string) =>
|
||||
`${status} at ${company}`;
|
||||
export const getDescription = (status?: string, company?: string): string => {
|
||||
if (status && company) return `${status} at ${company}`;
|
||||
if (status) return status;
|
||||
if (company) return `Employed at ${company}`;
|
||||
return 'DevBook Member';
|
||||
};
|
||||
|
||||
/** blank Dev serve as placeholder when initializing a new profile */
|
||||
export const blankDev: Dev = {
|
||||
id: '42',
|
||||
isActive: true,
|
||||
displayName: '',
|
||||
status: 'Developer',
|
||||
company: '',
|
||||
picture: '',
|
||||
description: '',
|
||||
location: '',
|
||||
skills: [],
|
||||
links: {
|
||||
/** class implementing IDev.
|
||||
* No constructor is provided.
|
||||
* new Dev() returns a placeholder used when initializing a new profile.
|
||||
* id is not specified to not overwrite document uid.
|
||||
*/
|
||||
export class Dev implements IDev {
|
||||
id?: string;
|
||||
isActive = true;
|
||||
displayName = '';
|
||||
status = 'Developer';
|
||||
company = '';
|
||||
avatarUrl = '';
|
||||
description = '';
|
||||
location = '';
|
||||
skills: string[] = [];
|
||||
github: string = '';
|
||||
links: Links = {
|
||||
website: '',
|
||||
instagram: '',
|
||||
facebook: '',
|
||||
|
|
@ -50,27 +59,28 @@ export const blankDev: Dev = {
|
|||
twitter: '',
|
||||
github: '',
|
||||
youtube: '',
|
||||
},
|
||||
bio: '',
|
||||
experiences: [],
|
||||
educations: [],
|
||||
repos: [],
|
||||
};
|
||||
};
|
||||
bio = '';
|
||||
experiences: Experience[] = [];
|
||||
educations: Education[] = [];
|
||||
repos: Repo[] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* sample Dev for development and tests
|
||||
*/
|
||||
export const dummyDev: Dev = {
|
||||
export const dummyDev: IDev = {
|
||||
id: '0',
|
||||
isActive: true,
|
||||
displayName: 'John Doe',
|
||||
status: 'Developer',
|
||||
company: 'Microsoft',
|
||||
picture:
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
|
||||
description: 'Developer at Microsoft',
|
||||
location: 'Seattle, WA',
|
||||
skills: ['HTML', 'CSS', 'JavaScript', 'Python'],
|
||||
github: '',
|
||||
links: {
|
||||
website: '#',
|
||||
instagram: 'http://insta.com',
|
||||
|
|
@ -146,4 +156,32 @@ export const dummyDev: Dev = {
|
|||
},
|
||||
],
|
||||
};
|
||||
export default Dev;
|
||||
|
||||
/** dummy devSummary profiles for debug and development only */
|
||||
export const developers: DevSummary[] = [
|
||||
{
|
||||
id: '0',
|
||||
displayName: 'John Doe',
|
||||
avatarUrl:
|
||||
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
|
||||
description: 'Developer at Microsoft',
|
||||
location: 'Seattle, WA',
|
||||
skills: ['HTML', 'CSS', 'JavaScript', 'Python'],
|
||||
status: 'Developer',
|
||||
company: 'Microsoft',
|
||||
},
|
||||
{
|
||||
id: '42',
|
||||
displayName: 'Ruidy Nemausat',
|
||||
avatarUrl:
|
||||
'https://lh3.googleusercontent.com/a-/AOh14GhncH95MWKwPR3TRKy4eVd4n6w0-fobe4dhiam2xA',
|
||||
description: 'Fullstack Engineer at DESY',
|
||||
|
||||
location: 'Hamburg, DE',
|
||||
skills: ['React', 'TypeScript', 'Redux', 'Nodejs'],
|
||||
status: 'Developer',
|
||||
company: 'Microsoft',
|
||||
},
|
||||
];
|
||||
|
||||
export default IDev;
|
||||
|
|
|
|||
|
|
@ -1,49 +1,39 @@
|
|||
import React, {FC} from 'react';
|
||||
// Redux
|
||||
import {compose} from 'redux';
|
||||
import {connect} from 'react-redux';
|
||||
import {firestoreConnect} from 'react-redux-firebase';
|
||||
import {RootState} from '../store';
|
||||
// Style
|
||||
import Header from '../components/Header';
|
||||
import DevProfile from '../components/DevProfile';
|
||||
import {DevSummary} from '../models/Dev';
|
||||
|
||||
interface IProps {
|
||||
developers: DevSummary[];
|
||||
}
|
||||
/**
|
||||
* Developers list page
|
||||
*/
|
||||
// const Developers: FC<DevSummary[]> = (developers) => {
|
||||
const Developers: FC = () => {
|
||||
const developers: DevSummary[] = [
|
||||
{
|
||||
id: '0',
|
||||
displayName: 'John Doe',
|
||||
picture:
|
||||
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
|
||||
description: 'Developer at Microsoft',
|
||||
location: 'Seattle, WA',
|
||||
skills: ['HTML', 'CSS', 'JavaScript', 'Python'],
|
||||
},
|
||||
{
|
||||
id: '42',
|
||||
displayName: 'Ruidy Nemausat',
|
||||
picture:
|
||||
'https://lh3.googleusercontent.com/a-/AOh14GhncH95MWKwPR3TRKy4eVd4n6w0-fobe4dhiam2xA',
|
||||
description: 'Fullstack Engineer at DESY',
|
||||
location: 'Hamburg, DE',
|
||||
skills: ['React', 'TypeScript', 'Redux', 'Nodejs'],
|
||||
},
|
||||
];
|
||||
const Developers: FC<IProps> = ({developers}) => (
|
||||
<section className="container">
|
||||
<Header
|
||||
title="Developers"
|
||||
lead="Browse and connect with developers"
|
||||
icon="connectdevelop"
|
||||
/>
|
||||
<div className="profiles">
|
||||
{developers?.map(dev => (
|
||||
// use spread operator to pass props
|
||||
<DevProfile key={dev.id} {...dev} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="container">
|
||||
<Header
|
||||
title="Developers"
|
||||
lead="Browse and connect with developers"
|
||||
icon="connectdevelop"
|
||||
/>
|
||||
<div className="profiles">
|
||||
{developers.map(dev => (
|
||||
// use spread operator to pass props
|
||||
<DevProfile key={dev.id} {...dev} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Developers;
|
||||
export default compose<FC>(
|
||||
firestoreConnect(() => ['users']), // or { collection: 'users' }
|
||||
connect((state: RootState, props) => ({
|
||||
developers: state.firestore.ordered.users,
|
||||
})),
|
||||
)(Developers);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import useForm from '../hooks';
|
|||
// Typing
|
||||
import Dev from '../models/Dev';
|
||||
import User from '../models/User';
|
||||
import Links from '../types/Links';
|
||||
import Links, {parseLink, getGithubLink} from '../types/Links';
|
||||
import IAlert, {formAlert} from '../types/Alert';
|
||||
|
||||
interface FormData {
|
||||
|
|
@ -52,6 +52,7 @@ const EditProfile: FC<IProps> = ({
|
|||
links,
|
||||
location,
|
||||
bio,
|
||||
github,
|
||||
}) => {
|
||||
const [showLinks, setShowLinks] = useState(false);
|
||||
const [alert, setAlert] = useState<IAlert>(formAlert);
|
||||
|
|
@ -63,7 +64,7 @@ const EditProfile: FC<IProps> = ({
|
|||
bio: bio ?? '',
|
||||
skills: skills?.toString() ?? '',
|
||||
website: links?.website ?? '',
|
||||
github: links?.github ?? '',
|
||||
github: github ?? '',
|
||||
facebook: links?.facebook ?? '',
|
||||
linkedin: links?.linkedin ?? '',
|
||||
instagram: links?.instagram ?? '',
|
||||
|
|
@ -89,13 +90,13 @@ const EditProfile: FC<IProps> = ({
|
|||
skills,
|
||||
}: FormData) => {
|
||||
const newLinks: Links = {
|
||||
website,
|
||||
instagram,
|
||||
facebook,
|
||||
linkedin,
|
||||
twitter,
|
||||
github,
|
||||
youtube,
|
||||
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(',');
|
||||
return {
|
||||
|
|
@ -103,6 +104,7 @@ const EditProfile: FC<IProps> = ({
|
|||
company,
|
||||
location,
|
||||
bio,
|
||||
github,
|
||||
links: newLinks,
|
||||
skills: newSkills,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
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,
|
||||
|
|
@ -6,6 +16,7 @@ import {
|
|||
faInstagram,
|
||||
faLinkedin,
|
||||
faTwitter,
|
||||
faYoutube,
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
import {
|
||||
faGlobe,
|
||||
|
|
@ -15,16 +26,29 @@ import {
|
|||
faEye,
|
||||
faCodeBranch,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import Dev, {dummyDev as dev} from '../models/Dev';
|
||||
// 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<Dev> = () => {
|
||||
const Profile: FC<IProps> = ({dev}) => {
|
||||
// display 404 page if dev is null
|
||||
if (dev === null) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
const fn = dev?.description;
|
||||
console.log(fn);
|
||||
|
||||
/** return the icon corresponding to the social name */
|
||||
const renderSocialIcon = (name: string): IconDefinition => {
|
||||
switch (name) {
|
||||
|
|
@ -38,88 +62,120 @@ const Profile: FC<Dev> = () => {
|
|||
return faLinkedin;
|
||||
case 'twitter':
|
||||
return faTwitter;
|
||||
case 'youtube':
|
||||
return faYoutube;
|
||||
default:
|
||||
return faGlobe;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
return dev === undefined ? (
|
||||
<div>Loading ... </div>
|
||||
) : (
|
||||
<section className="container">
|
||||
<a href="profiles.html" className="btn">
|
||||
<Link to={Routes.DEVELOPERS} className="btn">
|
||||
Back to profiles
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<div className="profile-grid my-1">
|
||||
<div className="profile-top bg-primary p-2">
|
||||
<img
|
||||
src="https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200"
|
||||
alt="Some guy"
|
||||
src={dev.avatarUrl}
|
||||
alt={dev.displayName}
|
||||
className="round-img my-1"
|
||||
/>
|
||||
<h1 className="large">{dev.displayName}</h1>
|
||||
<p className="lead">{dev.description}</p>
|
||||
<p className="lead">{getDescription(dev.status, dev.company)}</p>
|
||||
<p>{dev.location}</p>
|
||||
<div className="icons my-1">
|
||||
{Object.entries(dev.links).map(([icon, webAddress], i: number) => (
|
||||
<a href={webAddress} key={i}>
|
||||
<FontAwesomeIcon icon={renderSocialIcon(icon)} size="2x" />
|
||||
</a>
|
||||
))}
|
||||
{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}</p>
|
||||
<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.map((s: string, i: number) => (
|
||||
<div className="p-1" key={i}>
|
||||
<FontAwesomeIcon icon={faCheck} /> {s}
|
||||
</div>
|
||||
))}
|
||||
{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.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>
|
||||
{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.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>
|
||||
{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">
|
||||
|
|
@ -127,33 +183,58 @@ const Profile: FC<Dev> = () => {
|
|||
<FontAwesomeIcon icon={faGithub} /> GitHub Repos
|
||||
</h2>
|
||||
|
||||
{dev.repos.map((r: Repo, i: number) => (
|
||||
<div className="repo bg-white my-1 p-1">
|
||||
<div>
|
||||
<h4>
|
||||
<a href={r.link}>{r.name}</a>
|
||||
</h4>
|
||||
<p>{r.description}</p>
|
||||
</div>
|
||||
<div>
|
||||
<ul>
|
||||
<li className="badge badge-primary">
|
||||
<FontAwesomeIcon icon={faStar} /> Stars: 42
|
||||
</li>
|
||||
<li className="badge badge-dark">
|
||||
<FontAwesomeIcon icon={faEye} /> Watchers: 2
|
||||
</li>
|
||||
<li className="badge badge-light">
|
||||
<FontAwesomeIcon icon={faCodeBranch} /> Forks: 4
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{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">
|
||||
<div>
|
||||
<h4>
|
||||
<a href={r.link}>{r.name}</a>
|
||||
</h4>
|
||||
<p>{r.description}</p>
|
||||
</div>
|
||||
<div>
|
||||
<ul>
|
||||
<li className="badge badge-primary">
|
||||
<FontAwesomeIcon icon={faStar} /> Stars: 42
|
||||
</li>
|
||||
<li className="badge badge-dark">
|
||||
<FontAwesomeIcon icon={faEye} /> Watchers: 2
|
||||
</li>
|
||||
<li className="badge badge-light">
|
||||
<FontAwesomeIcon icon={faCodeBranch} /> Forks: 4
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
/**
|
||||
* 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;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Alert from '../components/Alert';
|
|||
import Header from '../components/Header';
|
||||
// Form
|
||||
import useForm from '../hooks';
|
||||
import Dev, {blankDev} from '../models/Dev';
|
||||
import {Dev} from '../models/Dev';
|
||||
|
||||
// extends withFirebaseProps type to ad profile info
|
||||
interface IProps extends Dev, WithFirebaseProps<User> {
|
||||
|
|
@ -57,7 +57,7 @@ const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
|
|||
firebase
|
||||
.createUser({email, password}, newUser(name, email))
|
||||
.then(() => {
|
||||
firebase.updateProfile(blankDev, {useSet: true, merge: true});
|
||||
firebase.updateProfile(new Dev(), {useSet: true, merge: true});
|
||||
resetForm();
|
||||
})
|
||||
.catch(err => setError(err));
|
||||
|
|
@ -82,7 +82,7 @@ const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
|
|||
)
|
||||
.then(() => {
|
||||
if (!exists)
|
||||
firebase.updateProfile(blankDev, {useSet: true, merge: true});
|
||||
firebase.updateProfile(new Dev(), {useSet: true, merge: true});
|
||||
});
|
||||
})
|
||||
.catch(err => setError(err));
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const Router: FC = () => (
|
|||
<Route exact path={Routes.SIGN_UP} component={SignUp} />
|
||||
<Route exact path={Routes.SIGN_IN} component={SignIn} />
|
||||
<Route exact path={Routes.DEVELOPERS} component={Developers} />
|
||||
<Route exact path={Routes.PROFILE} component={Profile} />
|
||||
<Route exact path={`${Routes.PROFILE}/:id`} component={Profile} />
|
||||
<PrivateRoute exact path={Routes.EDIT_PROFILE} component={EditProfile} />
|
||||
<PrivateRoute exact path={Routes.DASHBOARD} component={Dashboard} />
|
||||
<PrivateRoute
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
// Redux
|
||||
import {configureStore} from '@reduxjs/toolkit';
|
||||
// Firebase
|
||||
import {firebaseReducer, FirebaseReducer} from 'react-redux-firebase';
|
||||
import {
|
||||
firebaseReducer,
|
||||
FirebaseReducer,
|
||||
FirestoreReducer,
|
||||
} from 'react-redux-firebase';
|
||||
import {firestoreReducer} from 'redux-firestore';
|
||||
// Typing
|
||||
import {Schema} from './firebase/config';
|
||||
|
|
@ -17,6 +21,7 @@ const store = configureStore({
|
|||
// State type
|
||||
export interface RootState {
|
||||
firebase: FirebaseReducer.Reducer<Dev, Schema>;
|
||||
firestore: FirestoreReducer.Reducer;
|
||||
}
|
||||
|
||||
export default store;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ interface IAlert {
|
|||
text: string;
|
||||
}
|
||||
|
||||
/** standard alert displaying form status after submission */
|
||||
export const formAlert: IAlert = {
|
||||
show: false,
|
||||
color: 'danger',
|
||||
|
|
|
|||
|
|
@ -8,4 +8,22 @@ interface Links {
|
|||
youtube: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ensure link is formatted as http(s)//:...
|
||||
* @param link URI to process
|
||||
*/
|
||||
export const parseLink = (link: string): string => {
|
||||
if (link.slice(0, 4) === 'http') {
|
||||
return link;
|
||||
} else {
|
||||
return `http://${link}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param githubUsername
|
||||
*/
|
||||
export const getGithubLink = (githubUsername: string) =>
|
||||
`https://github.com/${githubUsername}`;
|
||||
|
||||
export default Links;
|
||||
|
|
|
|||
Loading…
Reference in a new issue