📑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:
Ruidy 2020-05-17 19:15:27 +02:00 committed by GitHub
parent 75c9888493
commit 309ee76a32
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 313 additions and 163 deletions

View file

@ -29,6 +29,15 @@ jobs:
name: Release name: Release
if: ${{ github.ref == 'refs/heads/master' }} if: ${{ github.ref == 'refs/heads/master' }}
runs-on: ubuntu-latest 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies

View file

@ -1,7 +1,12 @@
import React, {FC} from 'react'; import React, {FC} from 'react';
// Routing
import {Link} from 'react-router-dom';
// Style
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCheck} from '@fortawesome/free-solid-svg-icons'; import {faCheck} from '@fortawesome/free-solid-svg-icons';
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. * Present a dev profile succintly. Redirect to dev profile on click.
@ -10,23 +15,24 @@ import {DevSummary} from '../models/Dev';
const DevProfile: FC<DevSummary> = ({ const DevProfile: FC<DevSummary> = ({
id, id,
displayName, displayName,
picture, avatarUrl,
description, status,
company,
location, location,
skills, skills,
}) => ( }) => (
<div className="profile bg-light"> <div className="profile bg-light">
<img src={picture} alt={displayName} className="round-img" /> <img src={avatarUrl} alt={displayName} className="round-img" />
<div> <div>
<h2>{displayName}</h2> <h2>{displayName}</h2>
<p>{description}</p> <p>{getDescription(status, company)}</p>
<p>{location}</p> <p>{location}</p>
<a href="profile.html" className="btn btn-primary"> <Link to={`${Routes.PROFILE}/${id}`} className="btn btn-primary">
View Profile View Profile
</a> </Link>
</div> </div>
<ul> <ul>
{skills.map((s, i) => ( {skills?.map((s, i) => (
<li className="text-primary" key={i}> <li className="text-primary" key={i}>
<FontAwesomeIcon icon={faCheck} /> {s} <FontAwesomeIcon icon={faCheck} /> {s}
</li> </li>

View file

@ -5,10 +5,12 @@ import Repo from '../types/Repo';
/** Shorter dev interface */ /** Shorter dev interface */
export interface DevSummary { export interface DevSummary {
id: string; id?: string;
displayName: string; displayName: string;
picture: string; avatarUrl: string;
description: string; description: string;
status: string;
company: string;
location: string; location: string;
skills: string[]; skills: string[];
} }
@ -16,33 +18,40 @@ export interface DevSummary {
/** Full developer profile information. /** Full developer profile information.
* @extends DevSummary to avoid duplication * @extends DevSummary to avoid duplication
*/ */
interface Dev extends DevSummary { interface IDev extends DevSummary {
isActive: boolean; isActive: boolean;
bio: string; bio: string;
status: string; github: string;
company: string;
links: Links; links: Links;
experiences: Experience[]; experiences: Experience[];
educations: Education[]; educations: Education[];
repos: Repo[]; repos: Repo[];
} }
/** create profile tagline */ export const getDescription = (status?: string, company?: string): string => {
export const getDescription = (status: string, company: string) => if (status && company) return `${status} at ${company}`;
`${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 */ /** class implementing IDev.
export const blankDev: Dev = { * No constructor is provided.
id: '42', * new Dev() returns a placeholder used when initializing a new profile.
isActive: true, * id is not specified to not overwrite document uid.
displayName: '', */
status: 'Developer', export class Dev implements IDev {
company: '', id?: string;
picture: '', isActive = true;
description: '', displayName = '';
location: '', status = 'Developer';
skills: [], company = '';
links: { avatarUrl = '';
description = '';
location = '';
skills: string[] = [];
github: string = '';
links: Links = {
website: '', website: '',
instagram: '', instagram: '',
facebook: '', facebook: '',
@ -50,27 +59,28 @@ export const blankDev: Dev = {
twitter: '', twitter: '',
github: '', github: '',
youtube: '', youtube: '',
}, };
bio: '', bio = '';
experiences: [], experiences: Experience[] = [];
educations: [], educations: Education[] = [];
repos: [], repos: Repo[] = [];
}; }
/** /**
* sample Dev for development and tests * sample Dev for development and tests
*/ */
export const dummyDev: Dev = { export const dummyDev: IDev = {
id: '0', id: '0',
isActive: true, isActive: true,
displayName: 'John Doe', displayName: 'John Doe',
status: 'Developer', status: 'Developer',
company: 'Microsoft', company: 'Microsoft',
picture: avatarUrl:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200', 'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
description: 'Developer at Microsoft', description: 'Developer at Microsoft',
location: 'Seattle, WA', location: 'Seattle, WA',
skills: ['HTML', 'CSS', 'JavaScript', 'Python'], skills: ['HTML', 'CSS', 'JavaScript', 'Python'],
github: '',
links: { links: {
website: '#', website: '#',
instagram: 'http://insta.com', 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;

View file

@ -1,49 +1,39 @@
import React, {FC} from 'react'; 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 Header from '../components/Header';
import DevProfile from '../components/DevProfile'; import DevProfile from '../components/DevProfile';
import {DevSummary} from '../models/Dev'; import {DevSummary} from '../models/Dev';
interface IProps {
developers: DevSummary[];
}
/** /**
* Developers list page * Developers list page
*/ */
// const Developers: FC<DevSummary[]> = (developers) => { const Developers: FC<IProps> = ({developers}) => (
const Developers: FC = () => { <section className="container">
const developers: DevSummary[] = [ <Header
{ title="Developers"
id: '0', lead="Browse and connect with developers"
displayName: 'John Doe', icon="connectdevelop"
picture: />
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200', <div className="profiles">
description: 'Developer at Microsoft', {developers?.map(dev => (
location: 'Seattle, WA', // use spread operator to pass props
skills: ['HTML', 'CSS', 'JavaScript', 'Python'], <DevProfile key={dev.id} {...dev} />
}, ))}
{ </div>
id: '42', </section>
displayName: 'Ruidy Nemausat', );
picture:
'https://lh3.googleusercontent.com/a-/AOh14GhncH95MWKwPR3TRKy4eVd4n6w0-fobe4dhiam2xA',
description: 'Fullstack Engineer at DESY',
location: 'Hamburg, DE',
skills: ['React', 'TypeScript', 'Redux', 'Nodejs'],
},
];
return ( export default compose<FC>(
<section className="container"> firestoreConnect(() => ['users']), // or { collection: 'users' }
<Header connect((state: RootState, props) => ({
title="Developers" developers: state.firestore.ordered.users,
lead="Browse and connect with developers" })),
icon="connectdevelop" )(Developers);
/>
<div className="profiles">
{developers.map(dev => (
// use spread operator to pass props
<DevProfile key={dev.id} {...dev} />
))}
</div>
</section>
);
};
export default Developers;

View file

@ -21,7 +21,7 @@ import useForm from '../hooks';
// Typing // Typing
import Dev from '../models/Dev'; import Dev from '../models/Dev';
import User from '../models/User'; import User from '../models/User';
import Links from '../types/Links'; import Links, {parseLink, getGithubLink} from '../types/Links';
import IAlert, {formAlert} from '../types/Alert'; import IAlert, {formAlert} from '../types/Alert';
interface FormData { interface FormData {
@ -52,6 +52,7 @@ const EditProfile: FC<IProps> = ({
links, links,
location, location,
bio, bio,
github,
}) => { }) => {
const [showLinks, setShowLinks] = useState(false); const [showLinks, setShowLinks] = useState(false);
const [alert, setAlert] = useState<IAlert>(formAlert); const [alert, setAlert] = useState<IAlert>(formAlert);
@ -63,7 +64,7 @@ const EditProfile: FC<IProps> = ({
bio: bio ?? '', bio: bio ?? '',
skills: skills?.toString() ?? '', skills: skills?.toString() ?? '',
website: links?.website ?? '', website: links?.website ?? '',
github: links?.github ?? '', github: github ?? '',
facebook: links?.facebook ?? '', facebook: links?.facebook ?? '',
linkedin: links?.linkedin ?? '', linkedin: links?.linkedin ?? '',
instagram: links?.instagram ?? '', instagram: links?.instagram ?? '',
@ -89,13 +90,13 @@ const EditProfile: FC<IProps> = ({
skills, skills,
}: FormData) => { }: FormData) => {
const newLinks: Links = { const newLinks: Links = {
website, website: parseLink(website),
instagram, instagram: parseLink(instagram),
facebook, facebook: parseLink(facebook),
linkedin, linkedin: parseLink(linkedin),
twitter, twitter: parseLink(twitter),
github, github: getGithubLink(github),
youtube, youtube: parseLink(youtube),
}; };
const newSkills: string[] = skills?.split(','); const newSkills: string[] = skills?.split(',');
return { return {
@ -103,6 +104,7 @@ const EditProfile: FC<IProps> = ({
company, company,
location, location,
bio, bio,
github,
links: newLinks, links: newLinks,
skills: newSkills, skills: newSkills,
}; };

View file

@ -1,4 +1,14 @@
import React, {FC} from 'react'; 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 {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import { import {
faGithub, faGithub,
@ -6,6 +16,7 @@ import {
faInstagram, faInstagram,
faLinkedin, faLinkedin,
faTwitter, faTwitter,
faYoutube,
} from '@fortawesome/free-brands-svg-icons'; } from '@fortawesome/free-brands-svg-icons';
import { import {
faGlobe, faGlobe,
@ -15,16 +26,29 @@ import {
faEye, faEye,
faCodeBranch, faCodeBranch,
} from '@fortawesome/free-solid-svg-icons'; } 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 Experience from '../types/Experience';
import {getTimePeriod} from '../types/TimePeriod'; import {getTimePeriod} from '../types/TimePeriod';
import Education from '../types/Education'; import Education from '../types/Education';
import Repo from '../types/Repo'; import Repo from '../types/Repo';
interface IProps {
dev: IDev;
}
/** /**
* Dev personal profile as seen by other people. * 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 */ /** return the icon corresponding to the social name */
const renderSocialIcon = (name: string): IconDefinition => { const renderSocialIcon = (name: string): IconDefinition => {
switch (name) { switch (name) {
@ -38,88 +62,120 @@ const Profile: FC<Dev> = () => {
return faLinkedin; return faLinkedin;
case 'twitter': case 'twitter':
return faTwitter; return faTwitter;
case 'youtube':
return faYoutube;
default: default:
return faGlobe; return faGlobe;
} }
}; };
return ( return dev === undefined ? (
<div>Loading ... </div>
) : (
<section className="container"> <section className="container">
<a href="profiles.html" className="btn"> <Link to={Routes.DEVELOPERS} className="btn">
Back to profiles Back to profiles
</a> </Link>
<div className="profile-grid my-1"> <div className="profile-grid my-1">
<div className="profile-top bg-primary p-2"> <div className="profile-top bg-primary p-2">
<img <img
src="https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200" src={dev.avatarUrl}
alt="Some guy" alt={dev.displayName}
className="round-img my-1" className="round-img my-1"
/> />
<h1 className="large">{dev.displayName}</h1> <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> <p>{dev.location}</p>
<div className="icons my-1"> <div className="icons my-1">
{Object.entries(dev.links).map(([icon, webAddress], i: number) => ( {Object.entries(dev.links)
<a href={webAddress} key={i}> .sort()
<FontAwesomeIcon icon={renderSocialIcon(icon)} size="2x" /> .map(([icon, webAddress], i: number) => (
</a> <a
))} href={webAddress}
key={i}
target="_blank"
rel="noopener noreferrer"
>
<FontAwesomeIcon icon={renderSocialIcon(icon)} size="2x" />
</a>
))}
</div> </div>
</div> </div>
<div className="profile-about bg-light p-2"> <div className="profile-about bg-light p-2">
<h2 className="text-primary">{`${dev.displayName}'s Bio`}</h2> <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> <div className="line"></div>
<h2 className="text-primary">Skill Set</h2> <h2 className="text-primary">Skill Set</h2>
<div className="skills"> <div className="skills">
{dev.skills.map((s: string, i: number) => ( {dev.skills.length === 0
<div className="p-1" key={i}> ? 'Let us know about your skills!'
<FontAwesomeIcon icon={faCheck} /> {s} : dev.skills?.map((s: string, i: number) => (
</div> <div className="p-1" key={i}>
))} <FontAwesomeIcon icon={faCheck} /> {s}
</div>
))}
</div> </div>
</div> </div>
<div className="profile-exp bg-white p-2"> <div className="profile-exp bg-white p-2">
<h2 className="text-primary">Experiences</h2> <h2 className="text-primary">Experiences</h2>
{dev.experiences.map((exp: Experience, i: number) => ( {dev.experiences.length === 0 ? (
<div key={i}> <div>
<h3>{exp.company}</h3> <img
<p>{getTimePeriod(exp.from, exp.to)}</p> src={require('../static/img/404.jpg')}
<p> alt="no experiences"
<strong>Position: </strong> />
{exp.position}
</p>
<p>
<strong>Description: </strong>
{exp.description}
</p>
</div> </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>
<div className="profile-edu bg-white p-2"> <div className="profile-edu bg-white p-2">
<h2 className="text-primary">Education</h2> <h2 className="text-primary">Education</h2>
{dev.educations.map((edu: Education, i: number) => ( {dev.educations.length === 0 ? (
<div key={i}> <div>
<h3>{edu.school}</h3> <img src={require('../static/img/404.jpg')} alt="no educations" />
<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>
))} ) : (
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>
<div className="profile-github"> <div className="profile-github">
@ -127,33 +183,58 @@ const Profile: FC<Dev> = () => {
<FontAwesomeIcon icon={faGithub} /> GitHub Repos <FontAwesomeIcon icon={faGithub} /> GitHub Repos
</h2> </h2>
{dev.repos.map((r: Repo, i: number) => ( {dev.repos?.length === 0 ? (
<div className="repo bg-white my-1 p-1"> <div>
<div> <img
<h4> src={require('../static/img/404.jpg')}
<a href={r.link}>{r.name}</a> alt="no repositories"
</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>
))} ) : (
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>
</div> </div>
</section> </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;

View file

@ -12,7 +12,7 @@ import Alert from '../components/Alert';
import Header from '../components/Header'; import Header from '../components/Header';
// Form // Form
import useForm from '../hooks'; import useForm from '../hooks';
import Dev, {blankDev} from '../models/Dev'; import {Dev} from '../models/Dev';
// extends withFirebaseProps type to ad profile info // extends withFirebaseProps type to ad profile info
interface IProps extends Dev, WithFirebaseProps<User> { interface IProps extends Dev, WithFirebaseProps<User> {
@ -57,7 +57,7 @@ const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
firebase firebase
.createUser({email, password}, newUser(name, email)) .createUser({email, password}, newUser(name, email))
.then(() => { .then(() => {
firebase.updateProfile(blankDev, {useSet: true, merge: true}); firebase.updateProfile(new Dev(), {useSet: true, merge: true});
resetForm(); resetForm();
}) })
.catch(err => setError(err)); .catch(err => setError(err));
@ -82,7 +82,7 @@ const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
) )
.then(() => { .then(() => {
if (!exists) if (!exists)
firebase.updateProfile(blankDev, {useSet: true, merge: true}); firebase.updateProfile(new Dev(), {useSet: true, merge: true});
}); });
}) })
.catch(err => setError(err)); .catch(err => setError(err));

View file

@ -22,7 +22,7 @@ const Router: FC = () => (
<Route exact path={Routes.SIGN_UP} component={SignUp} /> <Route exact path={Routes.SIGN_UP} component={SignUp} />
<Route exact path={Routes.SIGN_IN} component={SignIn} /> <Route exact path={Routes.SIGN_IN} component={SignIn} />
<Route exact path={Routes.DEVELOPERS} component={Developers} /> <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.EDIT_PROFILE} component={EditProfile} />
<PrivateRoute exact path={Routes.DASHBOARD} component={Dashboard} /> <PrivateRoute exact path={Routes.DASHBOARD} component={Dashboard} />
<PrivateRoute <PrivateRoute

View file

@ -1,7 +1,11 @@
// Redux // Redux
import {configureStore} from '@reduxjs/toolkit'; import {configureStore} from '@reduxjs/toolkit';
// Firebase // Firebase
import {firebaseReducer, FirebaseReducer} from 'react-redux-firebase'; import {
firebaseReducer,
FirebaseReducer,
FirestoreReducer,
} from 'react-redux-firebase';
import {firestoreReducer} from 'redux-firestore'; import {firestoreReducer} from 'redux-firestore';
// Typing // Typing
import {Schema} from './firebase/config'; import {Schema} from './firebase/config';
@ -17,6 +21,7 @@ const store = configureStore({
// State type // State type
export interface RootState { export interface RootState {
firebase: FirebaseReducer.Reducer<Dev, Schema>; firebase: FirebaseReducer.Reducer<Dev, Schema>;
firestore: FirestoreReducer.Reducer;
} }
export default store; export default store;

View file

@ -4,6 +4,7 @@ interface IAlert {
text: string; text: string;
} }
/** standard alert displaying form status after submission */
export const formAlert: IAlert = { export const formAlert: IAlert = {
show: false, show: false,
color: 'danger', color: 'danger',

View file

@ -8,4 +8,22 @@ interface Links {
youtube: string; 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; export default Links;