From 309ee76a32928f686e68bbeba8fe9e56433c0c25 Mon Sep 17 00:00:00 2001 From: Ruidy Date: Sun, 17 May 2020 19:15:27 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=91Profile=20list=20(#11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .github/workflows/deploy.yml | 9 ++ src/components/DevProfile.tsx | 22 ++-- src/models/Dev.ts | 96 ++++++++++----- src/pages/Developers.tsx | 70 +++++------ src/pages/EditProfile.tsx | 20 +-- src/pages/Profile.tsx | 225 +++++++++++++++++++++++----------- src/pages/SignUp.tsx | 6 +- src/router/Router.tsx | 2 +- src/store/index.ts | 7 +- src/types/Alert.ts | 1 + src/types/Links.ts | 18 +++ 11 files changed, 313 insertions(+), 163 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 77a7f96..17f2dbc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 diff --git a/src/components/DevProfile.tsx b/src/components/DevProfile.tsx index 44c65d6..7006c06 100644 --- a/src/components/DevProfile.tsx +++ b/src/components/DevProfile.tsx @@ -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 = ({ id, displayName, - picture, - description, + avatarUrl, + status, + company, location, skills, }) => (
- {displayName} + {displayName}

{displayName}

-

{description}

+

{getDescription(status, company)}

{location}

- + View Profile - +
    - {skills.map((s, i) => ( + {skills?.map((s, i) => (
  • {s}
  • diff --git a/src/models/Dev.ts b/src/models/Dev.ts index 759112c..792b170 100644 --- a/src/models/Dev.ts +++ b/src/models/Dev.ts @@ -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; diff --git a/src/pages/Developers.tsx b/src/pages/Developers.tsx index 154ecf3..8900ab7 100644 --- a/src/pages/Developers.tsx +++ b/src/pages/Developers.tsx @@ -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 = (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 = ({developers}) => ( +
    +
    +
    + {developers?.map(dev => ( + // use spread operator to pass props + + ))} +
    +
    +); - return ( -
    -
    -
    - {developers.map(dev => ( - // use spread operator to pass props - - ))} -
    -
    - ); -}; - -export default Developers; +export default compose( + firestoreConnect(() => ['users']), // or { collection: 'users' } + connect((state: RootState, props) => ({ + developers: state.firestore.ordered.users, + })), +)(Developers); diff --git a/src/pages/EditProfile.tsx b/src/pages/EditProfile.tsx index a8e5142..f433c08 100644 --- a/src/pages/EditProfile.tsx +++ b/src/pages/EditProfile.tsx @@ -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 = ({ links, location, bio, + github, }) => { const [showLinks, setShowLinks] = useState(false); const [alert, setAlert] = useState(formAlert); @@ -63,7 +64,7 @@ const EditProfile: FC = ({ 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 = ({ 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 = ({ company, location, bio, + github, links: newLinks, skills: newSkills, }; diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index d9093f0..22c65d0 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -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 = () => { +const Profile: FC = ({dev}) => { + // display 404 page if dev is null + if (dev === null) { + return ; + } + + 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 = () => { return faLinkedin; case 'twitter': return faTwitter; + case 'youtube': + return faYoutube; default: return faGlobe; } }; - return ( + return dev === undefined ? ( +
    Loading ...
    + ) : (
    - + Back to profiles - +
    Some guy

    {dev.displayName}

    -

    {dev.description}

    +

    {getDescription(dev.status, dev.company)}

    {dev.location}

    - {Object.entries(dev.links).map(([icon, webAddress], i: number) => ( - - - - ))} + {Object.entries(dev.links) + .sort() + .map(([icon, webAddress], i: number) => ( + + + + ))}

    {`${dev.displayName}'s Bio`}

    -

    {dev.bio}

    +

    + {dev.bio.length === 0 + ? 'Add a short bio to present yourself!' + : dev.bio} +

    Skill Set

    - {dev.skills.map((s: string, i: number) => ( -
    - {s} -
    - ))} + {dev.skills.length === 0 + ? 'Let us know about your skills!' + : dev.skills?.map((s: string, i: number) => ( +
    + {s} +
    + ))}

    Experiences

    - {dev.experiences.map((exp: Experience, i: number) => ( -
    -

    {exp.company}

    -

    {getTimePeriod(exp.from, exp.to)}

    -

    - Position: - {exp.position} -

    -

    - Description: - {exp.description} -

    + {dev.experiences.length === 0 ? ( +
    + no experiences
    - ))} + ) : ( + dev.experiences.map((exp: Experience, i: number) => ( +
    +

    {exp.company}

    +

    {getTimePeriod(exp.from, exp.to)}

    +

    + Position: + {exp.position} +

    +

    + Description: + {exp.description} +

    +
    + )) + )}

    Education

    - {dev.educations.map((edu: Education, i: number) => ( -
    -

    {edu.school}

    -

    {getTimePeriod(edu.from, edu.to)}

    -

    - Degree: - {edu.degree} -

    -

    - Field: - {edu.field} -

    -

    - Description: - {edu.description} -

    + {dev.educations.length === 0 ? ( +
    + no educations
    - ))} + ) : ( + dev.educations.map((edu: Education, i: number) => ( +
    +

    {edu.school}

    +

    {getTimePeriod(edu.from, edu.to)}

    +

    + Degree: + {edu.degree} +

    +

    + Field: + {edu.field} +

    +

    + Description: + {edu.description} +

    +
    + )) + )}
    @@ -127,33 +183,58 @@ const Profile: FC = () => { GitHub Repos - {dev.repos.map((r: Repo, i: number) => ( -
    -
    -

    - {r.name} -

    -

    {r.description}

    -
    -
    -
      -
    • - Stars: 42 -
    • -
    • - Watchers: 2 -
    • -
    • - Forks: 4 -
    • -
    -
    + {dev.repos?.length === 0 ? ( +
    + no repositories
    - ))} + ) : ( + dev.repos.map((r: Repo, i: number) => ( +
    +
    +

    + {r.name} +

    +

    {r.description}

    +
    +
    +
      +
    • + Stars: 42 +
    • +
    • + Watchers: 2 +
    • +
    • + Forks: 4 +
    • +
    +
    +
    + )) + )}
    ); }; -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( + firestoreConnect(() => [`users/${id}`]), + connect(({firestore: {data}}: RootState) => ({ + dev: data.users && data.users[id], + })), + )(Profile); + + return ; +}; + +export default ProfileContainer; diff --git a/src/pages/SignUp.tsx b/src/pages/SignUp.tsx index c5d6ea3..ef7d19b 100644 --- a/src/pages/SignUp.tsx +++ b/src/pages/SignUp.tsx @@ -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 { @@ -57,7 +57,7 @@ const SignUp: FC = ({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 = ({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)); diff --git a/src/router/Router.tsx b/src/router/Router.tsx index 2d65705..31cd7aa 100644 --- a/src/router/Router.tsx +++ b/src/router/Router.tsx @@ -22,7 +22,7 @@ const Router: FC = () => ( - + ; + firestore: FirestoreReducer.Reducer; } export default store; diff --git a/src/types/Alert.ts b/src/types/Alert.ts index b637bb6..bd132c9 100644 --- a/src/types/Alert.ts +++ b/src/types/Alert.ts @@ -4,6 +4,7 @@ interface IAlert { text: string; } +/** standard alert displaying form status after submission */ export const formAlert: IAlert = { show: false, color: 'danger', diff --git a/src/types/Links.ts b/src/types/Links.ts index 0b8fc07..0e9c7fb 100644 --- a/src/types/Links.ts +++ b/src/types/Links.ts @@ -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;