revive ci; merge master

This commit is contained in:
Ruidy Nemausat 2020-05-17 08:32:51 +02:00
commit 27a0c5addf
65 changed files with 3846 additions and 154 deletions

View file

@ -5,6 +5,15 @@ jobs:
test:
name: Test
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
@ -16,3 +25,16 @@ jobs:
with:
start: yarn start
wait-on: 'http://localhost:3000'
release:
if: ${{ github.ref == 'master' }}
needs: test
name: Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: yarn build
- name: Install Firebase CLI tools
run: yarn global add firebase-tools
- name: Deploy
run: firebase deploy --token ${{ secrets.FIREBASE_TOKEN }}

9
.gitignore vendored
View file

@ -8,6 +8,8 @@
# testing
/coverage
/cypress/integration/examples
/cypress/fixtures/
/cypress/screenshots/
# production
/build
@ -24,3 +26,10 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.env
.firebase
firebase.json
firestore.indexes.json
*rules
.firebaserc

View file

@ -1,7 +1,11 @@
# DevBook
![Deploy](https://github.com/rjNemo/devbook_ts/workflows/Deploy/badge.svg?branch=master)
Social App for connecting with developers & tech enthusiasts.
🕸 [Check it out!](https://devbook.onrender.com/)
## Tests
### End-to-end
@ -16,11 +20,26 @@ yarn cypress
and edit your E2E test cases in `cypress/integration/` folder.
### Unit
Open test runner with:
```sh
yarn test
```
## Deployment
We use Github Actions to check push and pull requests.
The application is deployed on [Render](https://render.com) cloud platform.
It watches git `master` branch and automatic deploy if the diff passes the tests.
## Built With
- [ReactJs](https://reactjs.org/) - A JavaScript library for building user interfaces
- [Redux](https://redux.js.org/) - A predictable statea container for JavaScript apps
- [Firebase](https://fiirebase.google.com/) - Firebase helps mobile and web app teams succeed
- [Firebase](https://firebase.google.com/) - Firebase helps mobile and web app teams succeed
## Versioning

View file

@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View file

@ -0,0 +1,6 @@
describe('Landing page', () => {
it('contains app name', () => {
cy.visit('/');
cy.get('h1').contains('DevBook');
});
});

View file

@ -0,0 +1,6 @@
describe('App Layout', () => {
it('contains a navbar', () => {
cy.visit('/');
cy.get('nav');
});
});

View file

@ -1,6 +0,0 @@
describe('smoke', () => {
it('sees learn', () => {
cy.visit('/');
cy.get('a').contains('Learn');
});
});

117
firestore.indexes.json Normal file
View file

@ -0,0 +1,117 @@
{
"indexes": [
{
"collectionGroup": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "category",
"order": "ASCENDING"
},
{
"fieldPath": "avgRating",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "category",
"order": "ASCENDING"
},
{
"fieldPath": "numRatings",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "category",
"order": "ASCENDING"
},
{
"fieldPath": "price",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "city",
"order": "ASCENDING"
},
{
"fieldPath": "avgRating",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "city",
"order": "ASCENDING"
},
{
"fieldPath": "numRatings",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "city",
"order": "ASCENDING"
},
{
"fieldPath": "price",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "price",
"order": "ASCENDING"
},
{
"fieldPath": "avgRating",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "price",
"order": "ASCENDING"
},
{
"fieldPath": "numRatings",
"order": "DESCENDING"
}
]
}
],
"fieldOverrides": []
}

View file

@ -3,6 +3,13 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-brands-svg-icons": "^5.13.0",
"@fortawesome/free-regular-svg-icons": "^5.13.0",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/react-fontawesome": "^0.1.9",
"@reduxjs/toolkit": "^1.3.6",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
@ -10,11 +17,20 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.8",
"@types/react-router-dom": "^5.1.5",
"cypress": "^4.5.0",
"firebase": "^7.14.3",
"moment": "^2.25.3",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-google-button": "^0.7.1",
"react-redux": "^7.2.0",
"react-redux-firebase": "^3.4.0",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"typescript": "~3.7.2"
"redux-firestore": "^0.13.0",
"typescript": "^3.9.2"
},
"scripts": {
"start": "react-scripts start",
@ -38,4 +54,4 @@
"last 1 safari version"
]
}
}
}

View file

@ -4,40 +4,18 @@
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="theme-color" content="#17a2b8" />
<meta
name="description"
content="Web site created using create-react-app"
content="Create developer profiles, portfolio, share and get help from other
devs"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>DevBook</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View file

@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "DevBook",
"name": "DevBook | Social App for devs",
"icons": [
{
"src": "favicon.ico",
@ -20,6 +20,6 @@
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
"theme_color": "#17a2b8",
"background_color": "#000000"
}

View file

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View file

@ -1,9 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react';
import {render} from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
test('to pass ci', () => {});

View file

@ -1,26 +1,48 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
import React, {FC} from 'react';
// Routing
import {BrowserRouter} from 'react-router-dom';
import Router from './router/Router';
import NavBar from './components/NavBar';
// Redux
import {Provider, useSelector} from 'react-redux';
import store from './store';
// Firebase
import {ReactReduxFirebaseProvider, isLoaded} from 'react-redux-firebase';
import rrfProps from './store/firebase/config';
import {selectAuthState} from './store/firebase';
function App() {
/**
* Main App container
* Redux provides state management
* RRF to bind to Firebase
* */
const App: FC = () => {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
<Provider store={store}>
<ReactReduxFirebaseProvider {...rrfProps}>
<BrowserRouter>
<AuthApp />
</BrowserRouter>
</ReactReduxFirebaseProvider>
</Provider>
);
}
};
/**
* Display a loading screen while fetching authentication state
*/
const AuthApp: FC = () => {
const auth = useSelector(selectAuthState);
if (!isLoaded(auth)) {
// TODO: insert Splash Screen here
return <div>Loading...</div>;
}
return (
<>
<NavBar />
<Router />
</>
);
};
export default App;

16
src/components/Alert.tsx Normal file
View file

@ -0,0 +1,16 @@
import React, {FC} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons';
interface IProps {
text: string;
color?: string;
}
const Alert: FC<IProps> = ({text, color = 'danger'}) => (
<div className={`alert alert-${color}`}>
<FontAwesomeIcon icon={faExclamationTriangle} /> {text}
</div>
);
export default Alert;

View file

@ -0,0 +1,38 @@
import React, {FC} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCheck} from '@fortawesome/free-solid-svg-icons';
import {DevSummary} from '../models/Dev';
/**
* Present a dev profile succintly. Redirect to dev profile on click.
* @param props DevSummary object
*/
const DevProfile: FC<DevSummary> = ({
id,
displayName,
picture,
description,
location,
skills,
}) => (
<div className="profile bg-light">
<img src={picture} alt={displayName} className="round-img" />
<div>
<h2>{displayName}</h2>
<p>{description}</p>
<p>{location}</p>
<a href="profile.html" className="btn btn-primary">
View Profile
</a>
</div>
<ul>
{skills.map((s, i) => (
<li className="text-primary" key={i}>
<FontAwesomeIcon icon={faCheck} /> {s}
</li>
))}
</ul>
</div>
);
export default DevProfile;

View file

@ -0,0 +1,18 @@
import React, {FC} from 'react';
import Header from './Header';
interface IProps {
title: string;
lead: string;
icon?: string;
}
/** Header component displayed on form pages */
const FormHeader: FC<IProps> = props => (
<>
<Header {...props} />
<small>* marks required fields</small>
</>
);
export default FormHeader;

55
src/components/Header.tsx Normal file
View file

@ -0,0 +1,55 @@
import React, {FC} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faUser,
faCodeBranch,
faGraduationCap,
faExclamationTriangle,
} from '@fortawesome/free-solid-svg-icons';
import {faConnectdevelop} from '@fortawesome/free-brands-svg-icons';
interface IProps {
title: string;
lead: string;
icon?: string;
}
/**
* Header component
* @param title of the page
* @param lead description of the content
* @param icon to display (optional and default to faUser)
*/
const Header: FC<IProps> = ({title, lead, icon = 'faUser'}) => {
const RenderIcon = (icon: string) => {
if (icon === 'faUser') {
return <FontAwesomeIcon icon={faUser} />;
}
if (icon === 'connectdevelop') {
return <FontAwesomeIcon icon={faConnectdevelop} />;
}
if (icon === 'code-branch') {
return <FontAwesomeIcon icon={faCodeBranch} />;
}
if (icon === 'code') {
return null;
}
if (icon === 'graduation-cap') {
return <FontAwesomeIcon icon={faGraduationCap} />;
}
if (icon === 'not-found') {
return <FontAwesomeIcon icon={faExclamationTriangle} />;
}
};
return (
<>
<h1 className="large text-primary">{title}</h1>
<p className="lead">
{RenderIcon(icon)} {lead}
</p>
</>
);
};
export default Header;

92
src/components/NavBar.tsx Normal file
View file

@ -0,0 +1,92 @@
import React, {FC} 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 {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCode, faSignOutAlt, faUser} from '@fortawesome/free-solid-svg-icons';
// Typing
import User from '../models/User';
import Dev from '../models/Dev';
interface IProps extends Dev, WithFirebaseProps<User> {
isEmpty: boolean;
isLoaded: boolean;
}
/**
* Main Navbar serves navigation Routes.
*/
const NavBar: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
const publicLinks = (
<ul data-testid="publicLinks">
<li>
<Link to={Routes.DEVELOPERS} data-testid="devsLink">
Developers
</Link>
</li>
<li>
<Link to={Routes.SIGN_UP} data-testid="signupLink">
Register
</Link>
</li>
<li>
<Link to={Routes.SIGN_IN} data-testid="loginLink">
Login
</Link>
</li>
</ul>
);
const privateLinks = (
<ul data-testid="privateLinks">
<li>
<Link to={Routes.DEVELOPERS} data-testid="devsLink">
Developers
</Link>
</li>
<li>
<Link to={Routes.POSTS} data-testid="postsLink">
Posts
</Link>
</li>
<li>
<Link to={Routes.DASHBOARD} data-testid="dashboardLink">
<FontAwesomeIcon icon={faUser} />
<span className="hide-sm"> Dashboard</span>
</Link>
</li>
<li>
<Link
to={Routes.SIGN_IN}
data-testid="logoutLink"
onClick={() => firebase.logout()}
>
<FontAwesomeIcon icon={faSignOutAlt} />
<span className="hide-sm"> Log out</span>
</Link>
</li>
</ul>
);
/** Display appropriated links after loading given authenticated prop */
const RenderLinks =
isLoaded && !isEmpty && isActive ? privateLinks : publicLinks;
return (
<nav className="navbar bg-dark">
<h1>
<Link to={Routes.LANDING} data-testid="homeLink">
<FontAwesomeIcon icon={faCode} /> DevBook
</Link>
</h1>
{RenderLinks}
</nav>
);
};
/** connect HOC subscribes to the store */
export default enhance(NavBar);

19
src/constants/routes.ts Normal file
View file

@ -0,0 +1,19 @@
/**
* Register all Routes here for easy future modification.
* Paths must start with '/'
*/
enum Routes {
LANDING = '/',
SIGN_UP = '/signup',
SIGN_IN = '/signin',
DEVELOPERS = '/developers',
PROFILE = '/profile',
EDIT_PROFILE = '/edit-profile',
DASHBOARD = '/dashboard',
ADD_EXPERIENCE = '/add-experience',
ADD_EDUCATION = '/add-education',
POST = '/post',
POSTS = '/posts',
}
export default Routes;

12
src/constants/statuses.ts Normal file
View file

@ -0,0 +1,12 @@
const Statuses: string[] = [
'Developer',
'Junior Developer',
'Senior Developer',
'Manager',
'Student or Learning',
'Instructor or Teacher',
'Intern',
'Other',
];
export default Statuses;

34
src/hooks/index.ts Normal file
View file

@ -0,0 +1,34 @@
import {useState, ChangeEvent} from 'react';
/**
* provide onChange handler and reset function
* T is the initFormData object type
*
* @param initFormData initial state of the form
* @returns formData object,
* @returns handleChange function to pass to input tag
* @returns resetForm function to revert to initFormData
* */
const useForm = <T>(initFormData: T) => {
const [formData, setFormData] = useState<T>(initFormData);
/** update each input state value onChange */
const handleChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
): void =>
setFormData({
...formData,
[e.target.name]: e.target.value,
});
/** clean form after successful submition */
const resetForm = () => setFormData(initFormData);
// /** update checkboxes TODO: do it better ...*/
const handleCheckboxesChange = (e: ChangeEvent<HTMLInputElement>): void =>
setFormData({...formData, [e.target.name]: e.target.checked});
return {formData, handleChange, handleCheckboxesChange, resetForm};
};
export default useForm;

View file

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View file

@ -1,6 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import './static/css/style.min.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
@ -8,10 +8,7 @@ ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
document.getElementById('root'),
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

View file

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

149
src/models/Dev.ts Normal file
View file

@ -0,0 +1,149 @@
import Education from '../types/Education';
import Experience from '../types/Experience';
import Links from '../types/Links';
import Repo from '../types/Repo';
/** Shorter dev interface */
export interface DevSummary {
id: string;
displayName: string;
picture: string;
description: string;
location: string;
skills: string[];
}
/** Full developer profile information.
* @extends DevSummary to avoid duplication
*/
interface Dev extends DevSummary {
isActive: boolean;
bio: string;
status: string;
company: string;
links: Links;
experiences: Experience[];
educations: Education[];
repos: Repo[];
}
/** create profile tagline */
export const getDescription = (status: string, company: string) =>
`${status} at ${company}`;
/** 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: {
website: '',
instagram: '',
facebook: '',
linkedin: '',
twitter: '',
github: '',
youtube: '',
},
bio: '',
experiences: [],
educations: [],
repos: [],
};
/**
* sample Dev for development and tests
*/
export const dummyDev: Dev = {
id: '0',
isActive: true,
displayName: 'John Doe',
status: 'Developer',
company: 'Microsoft',
picture:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
description: 'Developer at Microsoft',
location: 'Seattle, WA',
skills: ['HTML', 'CSS', 'JavaScript', 'Python'],
links: {
website: '#',
instagram: 'http://insta.com',
facebook: '#',
linkedin: '#',
twitter: '#',
github: '#',
youtube: '#',
},
bio:
'Lorem ipsum, dolor sit amet consectetur adipisicing elit. Blanditiis unde quae vero enim adipisci voluptas magni sapiente reprehenderit error minima.',
experiences: [
{
id: 1,
company: 'Microsoft',
from: new Date(2011, 10),
to: 'Current',
position: 'Senior Developer',
location: 'USA',
description:
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Voluptas corrupti rem eius, accusantium ipsum vel eveniet magnam voluptatum? Minus, voluptatum!',
},
{
id: 0,
company: 'Sun Microsystems',
location: 'USA',
from: new Date(2004, 10),
to: new Date(2010, 11),
position: 'System Admin',
description:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Repellendus at rem totam sed qui! Quas.',
},
],
educations: [
{
id: 0,
school: 'University of Washington',
from: new Date(1993, 9),
to: new Date(1999, 6),
degree: 'Master',
field: 'Computer Science',
description:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Repellendus at rem totam sed qui! Quas.',
},
],
repos: [
{
name: 'Repo #1',
description:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Fugit,deserunt.',
link: '#',
stars: 42,
watchers: 2,
forks: 4,
},
{
name: 'Repo #2',
description:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Fugit,deserunt.',
link: '#',
stars: 21,
watchers: 1,
forks: 2,
},
{
name: 'Repo #3',
description:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Fugit,deserunt.',
link: '#',
stars: 50,
watchers: 32,
forks: 12,
},
],
};
export default Dev;

47
src/models/Post.ts Normal file
View file

@ -0,0 +1,47 @@
import Comment from '../types/Comment';
/**
* Post send by a dev
*/
interface Post {
id: string;
userID: string;
name: string;
text: string;
picture: string;
likes: string[];
comments: Comment[];
// date: Date;
}
/**
* sample Post for development and tests
*/
export const dummyPost: Post = {
id: '12',
userID: '42',
picture:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
name: 'John Doe',
text:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint possimus corporis sunt necessitatibus! Minus nesciunt soluta suscipit nobis. Amet accusamus distinctio cupiditate blanditiis dolor? Illo perferendis eveniet cum cupiditate aliquam?',
comments: [
{
picture:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
name: 'John Doe',
text:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Sintpossimus corporis sunt necessitatibus! Minus nesciunt solutasuscipit nobis. Amet accusamus distinctio cupiditate blanditiis dolor? Illo perferendis eveniet cum cupiditate aliquam?',
},
{
picture:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
name: 'Ruidy Nemo',
text:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Sintpossimus corporis sunt necessitatibus! Minus nesciunt solutasuscipit nobis. Amet accusamus distinctio cupiditate blanditiis dolor? Illo perferendis eveniet cum cupiditate aliquam?',
},
],
likes: ['0', '42'],
};
export default Post;

20
src/models/User.ts Normal file
View file

@ -0,0 +1,20 @@
interface User {
displayName: string;
email: string;
avatarUrl: string;
createdAt: Date;
}
/** User constructor */
export const newUser = (
displayName: string,
email: string,
avatarUrl: string = '',
): User => ({
displayName,
email,
avatarUrl,
createdAt: new Date(),
});
export default User;

188
src/pages/AddEducation.tsx Normal file
View file

@ -0,0 +1,188 @@
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);

189
src/pages/AddExperience.tsx Normal file
View file

@ -0,0 +1,189 @@
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);

146
src/pages/Dashboard.tsx Normal file
View file

@ -0,0 +1,146 @@
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);

49
src/pages/Developers.tsx Normal file
View file

@ -0,0 +1,49 @@
import React, {FC} from 'react';
import Header from '../components/Header';
import DevProfile from '../components/DevProfile';
import {DevSummary} from '../models/Dev';
/**
* 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'],
},
];
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;

313
src/pages/EditProfile.tsx Normal file
View file

@ -0,0 +1,313 @@
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';
// Typing
import Dev from '../models/Dev';
import User from '../models/User';
import Links 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,
}) => {
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: links?.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 = ({
status,
company,
location,
bio,
website,
instagram,
facebook,
linkedin,
twitter,
github,
youtube,
skills,
}: FormData) => {
const newLinks: Links = {
website,
instagram,
facebook,
linkedin,
twitter,
github,
youtube,
};
const newSkills: string[] = skills?.split(',');
return {
status,
company,
location,
bio,
links: newLinks,
skills: newSkills,
};
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault();
const updatedDev = 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) {
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);

31
src/pages/Landing.tsx Normal file
View file

@ -0,0 +1,31 @@
import React, {FC} from 'react';
import {Link} from 'react-router-dom';
import Routes from '../constants/routes';
import Header from '../components/Header';
/**
* Landing page
*/
const Landing: FC = () => (
<section className="landing">
<div className="dark-overlay">
<div className="landing-inner">
<Header
title="DevBook"
lead="Create developer profiles, portfolio, share and get help from other devs"
icon="code"
/>
<div className="buttons">
<Link to={Routes.SIGN_UP} className="btn btn-primary">
Sign up
</Link>
<Link to={Routes.SIGN_IN} className="btn btn-light">
Login
</Link>
</div>
</div>
</div>
</section>
);
export default Landing;

28
src/pages/NotFound.tsx Normal file
View file

@ -0,0 +1,28 @@
import React, {FC} from 'react';
import {Link} from 'react-router-dom';
import Header from '../components/Header';
import Routes from '../constants/routes';
const NotFound: FC = () => (
<section className="not-found">
<div className="dark-overlay">
<div className="landing-inner">
<Header
title="Nothing Here"
lead="Sorry the page requested does not exist."
icon="not-found"
/>
<div className="buttons">
<Link to={Routes.SIGN_UP} className="btn btn-primary">
Sign up
</Link>
<Link to={Routes.SIGN_IN} className="btn btn-light">
Login
</Link>
</div>
</div>
</div>
</section>
);
export default NotFound;

59
src/pages/Post.tsx Normal file
View file

@ -0,0 +1,59 @@
import React, {FC} from 'react';
import Post, {dummyPost as post} from '../models/Post';
import Comment from '../types/Comment';
/**
* Display a Post and the related comments. Shows a form to add a comment.
*/
const PostPage: FC<Post> = () => (
<section className="container">
<a href="posts.html" className="btn btn-light">
Back To Posts
</a>
<div className="post bg-white p-1 my-1">
<div>
<a href="profile.html">
<img className="round-img" src={post.picture} alt={post.name} />
<h4>{post.name}</h4>
</a>
</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">
<textarea
name="text"
cols={30}
rows={5}
placeholder="Comment on this post"
></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.picture} alt={c.name} />
<h4>{c.name}</h4>
</a>
</div>
<div>
<p className="my-1">{c.text}</p>
</div>
</div>
))}
</div>
</section>
);
export default PostPage;

58
src/pages/Posts.tsx Normal file
View file

@ -0,0 +1,58 @@
import React, {FC} from 'react';
import Post, {dummyPost as post} from '../models/Post';
import Header from '../components/Header';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faThumbsUp, faThumbsDown} from '@fortawesome/free-solid-svg-icons';
/**
* A Dev's Posts list
*/
const Posts: FC = () => {
const posts: Post[] = [post, post];
return (
<section className="container">
<Header title="Posts" lead="Welcome to the community" />
<div className="post-form">
<div className="post-form-header bg-primary">
<h3>Say Something</h3>
</div>
<form className="form my-1">
<textarea cols={30} rows={5} placeholder="Create a post"></textarea>
<input type="submit" value="Submit" className="btn btn-dark my-1" />
<div className="posts">
{posts.map((post: Post) => (
<div className="post bg-white p-1 my-1" key={post.id}>
<div>
<a href="profile.html">
<img
src={post.picture}
alt={post.name}
className="round-img"
/>
<h4>{post.name}</h4>
</a>
</div>
<div>
<p className="my-1">{post.text}</p>
<button className="btn btn-light">
<FontAwesomeIcon icon={faThumbsUp} /> {post.likes.length}
</button>
<button className="btn btn-light">
<FontAwesomeIcon icon={faThumbsDown} />
</button>
<a href="post.html" className="btn btn-primary">
Discussion
</a>
</div>
</div>
))}
</div>
</form>
</div>
</section>
);
};
export default Posts;

159
src/pages/Profile.tsx Normal file
View file

@ -0,0 +1,159 @@
import React, {FC} from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faGithub,
faFacebook,
faInstagram,
faLinkedin,
faTwitter,
} from '@fortawesome/free-brands-svg-icons';
import {
faGlobe,
IconDefinition,
faCheck,
faStar,
faEye,
faCodeBranch,
} from '@fortawesome/free-solid-svg-icons';
import Dev, {dummyDev as dev} from '../models/Dev';
import Experience from '../types/Experience';
import {getTimePeriod} from '../types/TimePeriod';
import Education from '../types/Education';
import Repo from '../types/Repo';
/**
* Dev personal profile as seen by other people.
*/
const Profile: FC<Dev> = () => {
/** 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;
default:
return faGlobe;
}
};
return (
<section className="container">
<a href="profiles.html" className="btn">
Back to profiles
</a>
<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"
className="round-img my-1"
/>
<h1 className="large">{dev.displayName}</h1>
<p className="lead">{dev.description}</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>
))}
</div>
</div>
<div className="profile-about bg-light p-2">
<h2 className="text-primary">{`${dev.displayName}'s Bio`}</h2>
<p>{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>
))}
</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>
</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>
</div>
))}
</div>
<div className="profile-github">
<h2 className="text-primary my-1">
<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>
</div>
))}
</div>
</div>
</section>
);
};
export default Profile;

109
src/pages/SignIn.tsx Normal file
View file

@ -0,0 +1,109 @@
import React, {FC, useState} from 'react';
// Redux
import {WithFirebaseProps} from 'react-redux-firebase';
import {enhance} from '../store/firebase';
// Routing
import {Link, Redirect} from 'react-router-dom';
import Routes from '../constants/routes';
// Style
import GoogleButton from 'react-google-button';
import Header from '../components/Header';
import Alert from '../components/Alert';
// Typing
import User from '../models/User';
// Form
import useForm from '../hooks';
import Dev from '../models/Dev';
interface InitFormData {
email: string;
password: string;
}
interface IProps extends Dev, WithFirebaseProps<User> {
isEmpty: boolean;
isLoaded: boolean;
}
/**
* Sign in form
*/
const SignIn: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
const [error, setError] = useState<any>(null);
// handle form data
const initFormData: InitFormData = {
email: '',
password: '',
};
const {formData, handleChange, resetForm} = useForm<InitFormData>(
initFormData,
);
const {email, password} = formData;
// prevent submitting invalid forms
const isDisabled: boolean = email === '' || password === '';
/** create user with password */
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
firebase
.login({email, password})
.then(() => resetForm())
.catch(err => setError(err));
};
const loginWithGoogle = () =>
firebase.login({provider: 'google', type: 'popup'});
// redirect to dashboard if connected
if (isLoaded && !isEmpty && isActive) {
return <Redirect to={Routes.DASHBOARD} />;
}
return (
<section className="container">
{error && <Alert text={error?.message} />}
<Header title="Sign In" lead="Sign into your account" />
<GoogleButton type="light" className="my-1" onClick={loginWithGoogle} />
<form onSubmit={handleSubmit} className="form">
<div className="form-group">
<input
name="email"
value={email}
onChange={handleChange}
placeholder="Email Address"
type="email"
required
autoFocus
/>
</div>
<div className="form-group">
<input
name="password"
value={password}
onChange={handleChange}
placeholder="Password"
type="password"
minLength={6}
required
/>
</div>
<input
type="submit"
value="Login"
className="btn btn-primary"
disabled={isDisabled}
/>
</form>
<p className="my-1">
Don't have an account? <Link to={Routes.SIGN_UP}>Sign up</Link>
</p>
</section>
);
};
/** subscribe to store and firebase */
export default enhance(SignIn);

166
src/pages/SignUp.tsx Normal file
View file

@ -0,0 +1,166 @@
import React, {FC, useState} from 'react';
// Routing
import {Link, Redirect} from 'react-router-dom';
import Routes from '../constants/routes';
// Redux
import {WithFirebaseProps} from 'react-redux-firebase';
import {enhance} from '../store/firebase';
import User, {newUser} from '../models/User';
// Style
import GoogleButton from 'react-google-button';
import Alert from '../components/Alert';
import Header from '../components/Header';
// Form
import useForm from '../hooks';
import Dev, {blankDev} from '../models/Dev';
// extends withFirebaseProps type to ad profile info
interface IProps extends Dev, WithFirebaseProps<User> {
isEmpty: boolean;
isLoaded: boolean;
}
interface InitFormData {
name: string;
email: string;
password: string;
password2: string;
}
/**
* Sign up form recieves firebase from withFirebase HOC
*/
const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
const [error, setError] = useState<any>(null);
// handle form data
const initFormData: InitFormData = {
name: '',
email: '',
password: '',
password2: '',
};
const {formData, handleChange, resetForm} = useForm<InitFormData>(
initFormData,
);
const {name, email, password, password2} = formData;
// prevent submitting invalid forms
const isDisabled: boolean = name === '' || email === '' || password === '';
/** create user with password */
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// pass the info to store into the second argument
firebase
.createUser({email, password}, newUser(name, email))
.then(() => {
firebase.updateProfile(blankDev, {useSet: true, merge: true});
resetForm();
})
.catch(err => setError(err));
};
const loginWithGoogle = () =>
firebase
.login({provider: 'google', type: 'popup'})
.then(() => {
// updateProfile only if user does not already exists in db
const email = firebase.auth().currentUser?.email;
let exists: boolean = false;
firebase
.firestore()
.collection('users/')
.where('email', '==', email)
.get()
.then(docs =>
docs.forEach(doc => {
exists = doc.data().isActive !== undefined;
}),
)
.then(() => {
if (!exists)
firebase.updateProfile(blankDev, {useSet: true, merge: true});
});
})
.catch(err => setError(err));
// redirect to dashboard if connected
if (isLoaded && !isEmpty && isActive) {
return <Redirect to={Routes.DASHBOARD} />;
}
return (
<section className="container">
{error && <Alert text={error?.message} />}
<Header title="Sign Up" lead="Create your account" />
<GoogleButton type="light" className="my-1" onClick={loginWithGoogle} />
<form className="form" onSubmit={handleSubmit}>
<div className="form-group">
<input
name="name"
value={name}
onChange={handleChange}
placeholder="Name"
type="text"
required
autoFocus
/>
</div>
<div className="form-group">
<input
name="email"
value={email}
onChange={handleChange}
placeholder="Email Address"
type="email"
required
/>
<small className="form-text">
This site uses Gravatar, so use a Gravatar email.
</small>
</div>
<div className="form-group">
<input
name="password"
value={password}
onChange={handleChange}
placeholder="Password"
type="password"
minLength={6}
required
/>
</div>
<div className="form-group">
<input
name="password2"
value={password2}
onChange={handleChange}
placeholder="Confirm Password"
type="password"
minLength={6}
required
/>
</div>
<input
type="submit"
value="Register"
className="btn btn-primary"
disabled={isDisabled}
/>
</form>
<p className="my-1">
Already have an account? <Link to={Routes.SIGN_IN}>Sign in</Link>
</p>
</section>
);
};
/** subscribe to store and firebase */
export default enhance(SignUp);

View file

@ -0,0 +1,7 @@
import SignUp from '../SignUp';
describe('Signup Page', () => {
it('calls loadUser function', () => {});
it('redirects to dashboard if signed up', () => {});
it('call signup function on click', () => {});
});

View file

@ -0,0 +1,50 @@
import React, {FC} from 'react';
// Routing
import {Route, Redirect} from 'react-router-dom';
import Routes from '../constants/routes';
// Redux
import {isLoaded, isEmpty} from 'react-redux-firebase';
import {useSelector} from 'react-redux';
import {RootState} from '../store';
interface IProps {
exact?: boolean;
path: string;
component: React.FC<any>;
}
/**
* Redirects to the login screen if you're not authenticated yet or
* if auth is not loaded yet
*/
const PrivateRoute: FC<IProps> = ({
component: Component,
exact,
path,
...rest
}) => {
const auth = useSelector((state: RootState) => state.firebase.auth);
const profile = useSelector((state: RootState) => state.firebase.profile);
const isActive = profile.isActive;
return (
<Route
exact={exact}
path={path}
{...rest}
render={({location, ...rest}) =>
isLoaded(auth) && !isEmpty(auth) && isActive ? (
<Component {...rest} />
) : (
<Redirect
to={{
pathname: Routes.SIGN_IN,
state: {from: location},
}}
/>
)
}
/>
);
};
/** subscribe to store and firebase */
export default PrivateRoute;

40
src/router/Router.tsx Normal file
View file

@ -0,0 +1,40 @@
import React, {FC} from 'react';
import {Switch, Route} from 'react-router-dom';
import Landing from '../pages/Landing';
import SignUp from '../pages/SignUp';
import SignIn from '../pages/SignIn';
import Developers from '../pages/Developers';
import Profile from '../pages/Profile';
import EditProfile from '../pages/EditProfile';
import Dashboard from '../pages/Dashboard';
import AddExperience from '../pages/AddExperience';
import AddEducation from '../pages/AddEducation';
import PostPage from '../pages/Post';
import Posts from '../pages/Posts';
import NotFound from '../pages/NotFound';
import Routes from '../constants/routes';
import PrivateRoute from './PrivateRoute';
/** Register navigation paths accessible */
const Router: FC = () => (
<Switch>
<Route exact path={Routes.LANDING} component={Landing} />
<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} />
<PrivateRoute exact path={Routes.EDIT_PROFILE} component={EditProfile} />
<PrivateRoute exact path={Routes.DASHBOARD} component={Dashboard} />
<PrivateRoute
exact
path={Routes.ADD_EXPERIENCE}
component={AddExperience}
/>
<PrivateRoute exact path={Routes.ADD_EDUCATION} component={AddEducation} />
<PrivateRoute exact path={Routes.POST} component={PostPage} />
<PrivateRoute exact path={Routes.POSTS} component={Posts} />
<Route component={NotFound} />
</Switch>
);
export default Router;

View file

@ -0,0 +1,21 @@
//Firebase
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
const CONFIG = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
databaseURL: process.env.REACT_APP_DB_URL,
projectId: process.env.REACT_APP_PROJECT_ID,
storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_MSG_SENDER_ID,
appId: process.env.REACT_APP_APP_ID,
measurementId: process.env.REACT_APP_MEASUREMENT_ID,
};
// initialize firebase services
firebase.initializeApp(CONFIG);
firebase.firestore();
export default firebase;

1
src/static/css/style.min.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
src/static/img/404.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
src/static/img/showcase.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

View file

@ -0,0 +1,39 @@
$primary-color: #17a2b8;
$dark-color: #343a40;
$light-color: #f4f4f4;
$danger-color: #dc3545;
$success-color: #28a745;
$dark: #333;
$max-width: 1100px;
// set text color based on background
@function set-text-color($color) {
@if (lightness($color) >60) {
@return $dark;
} @else {
@return white;
}
}
// set background and text color
@mixin set-background($color) {
background-color: $color;
color: set-text-color($color);
}
// margin and padding
$spaceamounts: (1, 2, 3, 4, 5);
@each $space in $spaceamounts {
.m-#{$space} {
margin: #{$space}rem;
}
.my-#{$space} {
margin: #{$space}rem 0;
}
.p-#{$space} {
padding: #{$space}rem;
}
.py-#{$space} {
padding: #{$space}rem 0;
}
}

View file

@ -0,0 +1,52 @@
.form {
&-group {
margin: 1.2rem 0;
}
&-text {
display: block;
margin-top: 0.3rem;
color: #888;
}
input[type='text'],
input[type='email'],
input[type='password'],
input[type='date'],
select,
textarea {
display: block;
width: 100%;
padding: 0.4rem;
font-size: 1.2rem;
border: 1px solid #ccc;
border-radius: 0.3rem;
}
input[type='submit'] {
font: inherit;
}
.social-input {
display: flex;
svg {
padding: 0.5rem;
width: 3rem;
&.fa-twitter {
color: #38a1f3;
}
&.fa-facebook {
color: #3b5998;
}
&.fa-instagram {
color: #3f729b;
}
&.fa-youtube {
color: #c4302b;
}
&.fa-linkedin {
color: #0077b5;
}
}
}
}

View file

@ -0,0 +1,76 @@
@media (max-width: 700px) {
.hide-sm {
display: none;
}
.container {
margin-top: 8rem;
}
// Text Styles
.x-large {
font-size: 3rem;
}
.large {
font-size: 2rem;
}
.lead {
font-size: 1rem;
}
// Navbar
.navbar {
flex-direction: column;
text-align: center;
ul {
text-align: center;
justify-content: center;
}
h1 {
margin-bottom: 1rem;
}
}
.dash-buttons a {
display: block;
width: 100%;
margin-bottom: 0.2rem;
}
// Profiles
.profile {
grid-template-columns: 1fr;
text-align: center;
ul {
display: none;
}
}
.profile-grid {
grid-template-areas:
"top"
"about"
"exp"
"edu"
"github";
}
.profile-about {
.skills {
flex-direction: column;
}
}
.post {
grid-template-columns: 1fr;
a,
button {
padding: 0.3rem 0.4rem;
}
}
}

183
src/static/scss/_utils.scss Normal file
View file

@ -0,0 +1,183 @@
@import '_config';
// Backgrounds
.bg {
&-primary {
@include set-background($primary-color);
}
&-light {
@include set-background($light-color);
border: #ccc 1px solid;
}
&-dark {
@include set-background($dark-color);
}
&-danger {
@include set-background($danger-color);
}
&-success {
@include set-background($success-color);
}
&-white {
@include set-background(white);
border: #ccc 1px solid;
}
}
// dark overlay
.dark-overlay {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.7);
}
// Text-styles
.x-large {
font-size: 4rem;
line-height: 1.2;
margin-bottom: 1rem;
}
.large {
font-size: 3rem;
line-height: 1.2;
margin-bottom: 1rem;
}
.lead {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.text-primary {
color: $primary-color;
}
// Buttons
.btn {
display: inline-block;
background-color: $light-color;
color: $dark;
padding: 0.4rem 1.3rem;
border: none;
cursor: pointer;
font-size: 1rem;
margin-right: 0.5rem;
outline: none;
transition: all 0.3s ease-in;
&.btn-primary {
@include set-background($primary-color);
&:hover {
background: lighten($primary-color, 5%);
}
}
&.btn-dark {
@include set-background($dark-color);
&:hover {
background: lighten($dark-color, 5%);
}
}
&.btn-light {
@include set-background($light-color);
&:hover {
background: lighten($dark-color, 20%);
color: white;
}
}
&.btn-danger {
@include set-background($danger-color);
&:hover {
background: lighten($danger-color, 5%);
}
}
&.btn-success {
@include set-background($success-color);
&:hover {
background: lighten($success-color, 5%);
}
}
}
// Container
.container {
max-width: $max-width;
margin: auto;
overflow: hidden;
padding: 0 2rem;
margin-top: 5rem;
margin-bottom: 3rem;
}
// Alerts
.alert {
padding: 0.8rem;
margin: 1rem;
opacity: 0.9;
background-color: $light-color;
color: $dark;
&.alert-primary {
@include set-background($primary-color);
}
&.alert-dark {
@include set-background($dark-color);
}
&.alert-success {
@include set-background($success-color);
}
&.alert-danger {
@include set-background($danger-color);
}
}
.round-img {
border-radius: 50%;
}
.line {
height: 1px;
background: #ccc;
margin: 1.5rem 0;
}
// badge
.badge {
font-size: 0.8rem;
padding: 0.1rem;
text-align: center;
margin: 0.3rem;
background: $light-color;
color: $dark;
border-radius: 1rem;
&.badge-primary {
@include set-background($primary-color);
}
&.badge-dark {
@include set-background($dark-color);
}
&.badge-success {
@include set-background($success-color);
}
&.badge-danger {
@include set-background($danger-color);
}
}
// Table
.table {
th,
td {
padding: 1rem;
text-align: left;
}
th {
background: $light-color;
}
}

198
src/static/scss/style.scss Normal file
View file

@ -0,0 +1,198 @@
@import '_config';
@import '_utils';
@import '_form';
@import '_mobile';
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Raleway', sans-serif;
font-size: 1rem;
line-height: 1.6;
background-color: white;
color: $dark;
}
a {
text-decoration: none;
color: $primary-color;
}
ul {
list-style: none;
}
img {
width: 100%;
}
// Navbar
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.7rem 2rem;
position: fixed;
z-index: 1;
width: 100%;
top: 0;
border-bottom: solid 1px $primary-color;
opacity: 0.9;
ul {
display: flex;
}
a {
color: white;
padding: 0.45rem;
margin: 0 0.25rem;
&:hover {
color: $primary-color;
}
}
}
// landing
.landing {
position: relative;
background: url('../img/showcase.jpg') no-repeat center center/cover;
height: 100vh;
&-inner {
color: white;
height: 100%;
display: flex;
flex-direction: column;
width: 80%;
margin: auto;
align-items: center;
justify-content: center;
text-align: center;
}
}
// profiles
.profile {
display: grid;
grid-template-columns: 2fr 4fr 2fr;
grid-gap: 2rem;
padding: 1rem;
line-height: 1.8;
margin-bottom: 1rem;
align-items: center;
}
// Profile Page
.profile-grid {
display: grid;
grid-template-areas: 'top top' 'about about' 'exp edu' 'github github';
grid-gap: 1rem;
.profile-top {
grid-area: top;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
img {
width: 250px;
}
.icons a {
color: white;
margin: 0 0.3rem;
&:hover {
color: $dark-color;
}
}
}
.profile-about {
grid-area: about;
text-align: center;
.skills {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
}
.profile-exp {
grid-area: exp;
}
.profile-edu {
grid-area: edu;
}
.profile-exp,
.profile-edu {
& > div {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: #ccc 1px dotted;
&:last-child {
border: none;
}
p {
margin: 0.5rem 0;
}
}
}
.profile-github {
grid-area: github;
.repo {
display: flex;
& > div:first-child {
flex: 7;
flex-basis: 70%;
}
& > div:last-child {
flex: 3;
flex-basis: 30%;
}
}
}
}
// Posts
.post-form-header {
padding: 0.5rem;
}
.post {
display: grid;
grid-template-columns: 1fr 4fr;
grid-gap: 2rem;
align-items: center;
& > div:first-child {
text-align: center;
}
img {
width: 150px;
}
}
.not-found {
position: relative;
background: url('../img/404.jpg') no-repeat center center/cover;
height: 100vh;
}

36
src/store/auth/index.ts Normal file
View file

@ -0,0 +1,36 @@
// 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;

View file

@ -0,0 +1,27 @@
// Redux
import {createFirestoreInstance} from 'redux-firestore';
import store from '..';
// Firebase
import firebase from '../../services/firebase';
// Typing
import Dev from '../../models/Dev';
// react-redux-firebase config
const RRF_CONFIG = {
userProfile: 'users',
useFirestoreForProfile: true,
};
// object required by RRFProvider
const rrfProps = {
firebase,
config: RRF_CONFIG,
dispatch: store.dispatch,
createFirestoreInstance,
};
// Firestore Schema
export interface Schema {
devs: Dev;
}
export default rrfProps;

View file

@ -0,0 +1,13 @@
import {FC} from 'react';
// Redux
import {compose} from '@reduxjs/toolkit';
import {connect} from 'react-redux';
import {withFirebase} from 'react-redux-firebase';
import {RootState} from '..';
/** export firebase authentication */
export const selectAuthState = (state: RootState) => state.firebase.auth;
/** export current user profile */
export const selectProfile = (state: RootState) => state.firebase.profile;
/** subscribe to firebase and profile */
export const enhance = compose<FC>(connect(selectProfile), withFirebase);

22
src/store/index.ts Normal file
View file

@ -0,0 +1,22 @@
// Redux
import {configureStore} from '@reduxjs/toolkit';
// Firebase
import {firebaseReducer, FirebaseReducer} from 'react-redux-firebase';
import {firestoreReducer} from 'redux-firestore';
// Typing
import {Schema} from './firebase/config';
import Dev from '../models/Dev';
const store = configureStore({
reducer: {
firebase: firebaseReducer,
firestore: firestoreReducer,
},
});
// State type
export interface RootState {
firebase: FirebaseReducer.Reducer<Dev, Schema>;
}
export default store;

13
src/types/Alert.ts Normal file
View file

@ -0,0 +1,13 @@
interface IAlert {
show: boolean;
color: string;
text: string;
}
export const formAlert: IAlert = {
show: false,
color: 'danger',
text: 'Something went wrong',
};
export default IAlert;

9
src/types/Comment.ts Normal file
View file

@ -0,0 +1,9 @@
interface Comment {
// userID: string;
text: string;
name: string;
picture: string;
// date: Date;
}
export default Comment;

13
src/types/Education.ts Normal file
View file

@ -0,0 +1,13 @@
import TimePeriod from '../types/TimePeriod';
interface Education {
id: number;
school: string;
degree: string;
from: TimePeriod;
to: TimePeriod;
field: string;
description: string;
}
export default Education;

13
src/types/Experience.ts Normal file
View file

@ -0,0 +1,13 @@
import TimePeriod from '../types/TimePeriod';
interface Experience {
id: number;
company: string;
from: TimePeriod;
to: TimePeriod;
position: string;
description: string;
location: string;
}
export default Experience;

11
src/types/Links.ts Normal file
View file

@ -0,0 +1,11 @@
interface Links {
website: string;
instagram: string;
facebook: string;
linkedin: string;
twitter: string;
github: string;
youtube: string;
}
export default Links;

10
src/types/Repo.ts Normal file
View file

@ -0,0 +1,10 @@
interface Repo {
name: string;
description: string;
link: string;
stars: number;
watchers: number;
forks: number;
}
export default Repo;

21
src/types/TimePeriod.ts Normal file
View file

@ -0,0 +1,21 @@
import moment from 'moment';
type TimePeriod = string | Date | 'Current';
/** format exp date to be used */
export const parseDate = (date: TimePeriod): string => {
if (date === 'Current') {
return date;
}
return moment(date).format('MMM. YYYY');
};
/**
* Formats a time period assignment: experience or education.
* @param from Start of the assignment. Must be a Date
* @param to End of the assignment. Can be "Current"
*/
export const getTimePeriod = (from: TimePeriod, to: TimePeriod): string =>
`${parseDate(from)} - ${parseDate(to)}`;
export default TimePeriod;

63
tslint.json Normal file
View file

@ -0,0 +1,63 @@
{
"rules": {
"adjacent-overload-signatures": true,
"ban-comma-operator": true,
"no-namespace": true,
"no-parameter-reassignment": true,
"no-reference": true,
"no-unnecessary-type-assertion": true,
"label-position": true,
"no-conditional-assignment": true,
"no-construct": true,
"no-duplicate-super": true,
"no-duplicate-switch-case": true,
"no-duplicate-variable": [
true,
"check-parameters"
],
"no-shadowed-variable": true,
"no-empty": [
true,
"allow-empty-catch"
],
"no-floating-promises": true,
"no-implicit-dependencies": true,
"no-invalid-this": true,
"no-string-throw": true,
"no-unsafe-finally": true,
"no-void-expression": [
true,
"ignore-arrow-function-shorthand"
],
"no-duplicate-imports": true,
// Warn when an empty interface is defined. These are generally not useful.
"no-empty-interface": {
"severity": "warning"
},
"no-import-side-effect": {
"severity": "warning"
},
"no-var-keyword": {
"severity": "warning"
},
"triple-equals": {
"severity": "warning"
},
"deprecation": {
"severity": "warning"
},
"prefer-for-of": {
"severity": "warning"
},
"unified-signatures": {
"severity": "warning"
},
"prefer-const": {
"severity": "warning"
},
"trailing-comma": {
"severity": "warning"
}
},
"defaultSeverity": "error"
}

698
yarn.lock
View file

@ -956,7 +956,7 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.5.1", "@babel/runtime@^7.7.4":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.4":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
@ -1082,6 +1082,289 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@firebase/analytics-types@0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.3.0.tgz#33c3f695313b561d48d18d663a20f20362d3ee7c"
integrity sha512-0AJ6xn53Qn0D/YOVHHvlWFfnzzRSdd98Lr8Oqe1PJ2HPIN+o7qf03YmOG7fLpR1uplcWd+7vGKmxUrN3jKUBwg==
"@firebase/analytics@0.3.4":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.3.4.tgz#64146d8cb36c2239ba39330459022c3e8d861489"
integrity sha512-x5Hxj3B9Zm4H6CEjMD/J86WjmiX9C6AhBBltaYzWMtqkqa/WvvWMicl4MpwZXjOdBbOd286oGgJPFqQMUYI/WQ==
dependencies:
"@firebase/analytics-types" "0.3.0"
"@firebase/component" "0.1.11"
"@firebase/installations" "0.4.9"
"@firebase/logger" "0.2.3"
"@firebase/util" "0.2.46"
tslib "1.11.1"
"@firebase/app-types@0.6.0":
version "0.6.0"
resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.6.0.tgz#8dcc3e793c6983e9d54f7eb623a7618c05f2d94c"
integrity sha512-ld6rzjXk/SUauHiQZJkeuSJpxIZ5wdnWuF5fWBFQNPaxsaJ9kyYg9GqEvwZ1z2e6JP5cU9gwRBlfW1WkGtGDYA==
"@firebase/app@0.6.3":
version "0.6.3"
resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.6.3.tgz#dade13b381f3b02b9a3847c57a0eec584629d2df"
integrity sha512-59Q/XNi+QyV1GOvxN+BusoKjqDKUjDupePDhlT6SqvFdvb03TjG03fSfurhXGXmTk6f500aOIyVJ8UlYpTYrsg==
dependencies:
"@firebase/app-types" "0.6.0"
"@firebase/component" "0.1.11"
"@firebase/logger" "0.2.3"
"@firebase/util" "0.2.46"
dom-storage "2.1.0"
tslib "1.11.1"
xmlhttprequest "1.8.0"
"@firebase/auth-interop-types@0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.1.4.tgz#e81589f58508630a5bffa604d7c949a0d01ea97b"
integrity sha512-CLKNS84KGAv5lRnHTQZFWoR11Ti7gIPFirDDXWek/fSU+TdYdnxJFR5XSD4OuGyzUYQ3Dq7aVj5teiRdyBl9hA==
"@firebase/auth-types@0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.10.0.tgz#9403633e723336055fad4bbf5e4c9fe3c55f8d3f"
integrity sha512-VuW7c+RAk3AYPU0Hxmun3RzXn7fbJDdjQbxvvpRMnQ9zrhk8mH42cY466M0n4e/UGQ+0smlx5BqZII8aYQ5XPg==
"@firebase/auth@0.14.5":
version "0.14.5"
resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.14.5.tgz#52a313579a711e70cefa735045242be1e29aefdc"
integrity sha512-76ejEQrJ81s2ZI2RV/AoZnw3sDl7dZSpaJJtPlhqlahymtQ2sSeAZJAmECcTB27PF6EeCdRhB9qOIKGAEAhbJg==
dependencies:
"@firebase/auth-types" "0.10.0"
"@firebase/component@0.1.11":
version "0.1.11"
resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.1.11.tgz#0341f2dd99eac32a28d2b674eef9be9b7c5c9ad9"
integrity sha512-HZ0fwtv8/b3KV4NUOqlcIr03+CpBKW0F1Jo6/HJ39AutS6XXbM2jtpXOd1wMq9lbhBHgEwt1sMPNKoPR1bFflQ==
dependencies:
"@firebase/util" "0.2.46"
tslib "1.11.1"
"@firebase/database-types@0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.5.0.tgz#603a0865c3180a9ffb6f5fa065d156387385a74d"
integrity sha512-6/W3frFznYOALtw2nrWVPK2ytgdl89CzTqVBHCCGf22wT6uKU63iDBo+Nw+7olFGpD15O0zwYalFIcMZ27tkew==
dependencies:
"@firebase/app-types" "0.6.0"
"@firebase/database@0.6.2":
version "0.6.2"
resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.6.2.tgz#d8ef8134d2ce71d5af2a948f8d2fd1ae36a48a12"
integrity sha512-0D0WOqYlNg3NMi0hJPx18tun6FMfr31d1dZB0Lai0K5jScBhPr2h4Fy7yp5lyOklwDSAoBYxmpX4nzHuDheL9Q==
dependencies:
"@firebase/auth-interop-types" "0.1.4"
"@firebase/component" "0.1.11"
"@firebase/database-types" "0.5.0"
"@firebase/logger" "0.2.3"
"@firebase/util" "0.2.46"
faye-websocket "0.11.3"
tslib "1.11.1"
"@firebase/firestore-types@1.10.1":
version "1.10.1"
resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-1.10.1.tgz#bf018f9c495f470592de745389474dc1c2960d3f"
integrity sha512-vyKdm+AYUFT8XeUX62IOqaqPFCs/mAMoSEsqIz9HnSVsqCw/IocNjtjSa+3M80kRw4V8fI7JI+Xz6Wg5VJXLqA==
"@firebase/firestore@1.14.3":
version "1.14.3"
resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-1.14.3.tgz#675e0c26db056eee140e06bb17ecc91f21cc4390"
integrity sha512-vmkXa5Msumutf0ZQjF8AQQwXr4mXI7D8TBbI44w+CMQEnKcD5MW7Dr1SmCTWy8+kNAAFwdA6lCiUtDY5Gx/Hlw==
dependencies:
"@firebase/component" "0.1.11"
"@firebase/firestore-types" "1.10.1"
"@firebase/logger" "0.2.3"
"@firebase/util" "0.2.46"
"@firebase/webchannel-wrapper" "0.2.40"
"@grpc/grpc-js" "0.8.1"
"@grpc/proto-loader" "^0.5.0"
tslib "1.11.1"
"@firebase/functions-types@0.3.16":
version "0.3.16"
resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.3.16.tgz#be0362d7f61648fdf36a7d95de239eddee88f931"
integrity sha512-kHhBvSYiY2prY4vNQCALYs1+OruTdylvGemHG6G6Bs/rj3qw7ui3WysBsDU/rInJitHIcsZ35qrtanoJeQUIXQ==
"@firebase/functions@0.4.43":
version "0.4.43"
resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.4.43.tgz#4307e56b4ae9b6a5c92e5227ae351896c7a0c14d"
integrity sha512-9cBGRr5JPcshtdUPpWuzsRIVPcWWNncK97QWBoFakVymPjvFNS3r0ZxD3hSUr9i05VrZdrqJfdljTNm8eEmJiA==
dependencies:
"@firebase/component" "0.1.11"
"@firebase/functions-types" "0.3.16"
"@firebase/messaging-types" "0.4.4"
isomorphic-fetch "2.2.1"
tslib "1.11.1"
"@firebase/installations-types@0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.3.3.tgz#f2e49e73afaeb7b352250365d0d90dff0b792592"
integrity sha512-XvWhPPAGeZlc+CfCA8jTt2pv19Jovi/nUV73u30QbjBbk5xci9bp5I29aBZukHsR6YNBjFCLSkLPbno4m/bLUg==
"@firebase/installations@0.4.9":
version "0.4.9"
resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.4.9.tgz#b346d1392919b45216b11f938a689e9cf44aa70e"
integrity sha512-5oY3iycidoK2MhNl4GiFYn/B9rbW69VLpH54EGEFl1UruGk464WyqC7RhJxYl8bUkFwZ4gg99MXMq/JhF0vcJA==
dependencies:
"@firebase/component" "0.1.11"
"@firebase/installations-types" "0.3.3"
"@firebase/util" "0.2.46"
idb "3.0.2"
tslib "1.11.1"
"@firebase/logger@0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.2.3.tgz#6a0eea0c2ce0a609f2965c82ce793ce5b7b32572"
integrity sha512-PrYcr1bWF+QpVnFxvNSZYBAzgL1WJFWIOvoLAfvRoXiinwqh1jbePN6lXbX7c8THaNUelEYIUOzDPdJ4IZ5+Sw==
"@firebase/messaging-types@0.4.4":
version "0.4.4"
resolved "https://registry.yarnpkg.com/@firebase/messaging-types/-/messaging-types-0.4.4.tgz#bef66157bdd3ddaafd6d48f1c5ee973fdc385f84"
integrity sha512-JGtkr+1A1Dw7+yCqQigqBfGKtq0gTCruFScBD4MVjqZHiqGIYpnQisWnpGbkzPR6aOt6iQxgwxUhHG1ulUQGeg==
"@firebase/messaging@0.6.15":
version "0.6.15"
resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.6.15.tgz#e0f693338e4d6f10a28a94127765c70f643915cd"
integrity sha512-WswV3JtxAgqc0LPQtIBdMWJdMhVZu7gKF6MO5ETIpNaLZZ0QayYNu5+G9btoZz218HB/gvUp2NFX43OWAsqdZw==
dependencies:
"@firebase/component" "0.1.11"
"@firebase/installations" "0.4.9"
"@firebase/messaging-types" "0.4.4"
"@firebase/util" "0.2.46"
idb "3.0.2"
tslib "1.11.1"
"@firebase/performance-types@0.0.12":
version "0.0.12"
resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.12.tgz#15fa79e296b502e21054a66c9e7ded59398fd8a7"
integrity sha512-eIDF7CHetOE5sc+hCaUebEn/2Aiaju7UkgZDTl7lNQHz5fK9wJ/11HaE8WdnDr//ngS3lQAGC2RB4lAZeEWraA==
"@firebase/performance@0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.3.3.tgz#5141c8e4452777238847515a53db309cbf446a58"
integrity sha512-YcoMnJWnlSQwi+eL1BDLWK7/sMlFoT7+TSJjN/C5loOZ3HWLATziGzevQSZkpajyXZ8nOylVhEGHABLHM0qqNA==
dependencies:
"@firebase/component" "0.1.11"
"@firebase/installations" "0.4.9"
"@firebase/logger" "0.2.3"
"@firebase/performance-types" "0.0.12"
"@firebase/util" "0.2.46"
tslib "1.11.1"
"@firebase/polyfill@0.3.35":
version "0.3.35"
resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.35.tgz#b3acca690ab5906558494bac9025ea5f41ce18d0"
integrity sha512-O04KLyrHFXnA8Xsx+zEBlHu6iHWWhXNtOIE9WhWZO+D9onVjNEY3l7KtXvwpH/b+R1PE0Uyxy0cSGK9f5el6HQ==
dependencies:
core-js "3.6.5"
promise-polyfill "8.1.3"
whatwg-fetch "2.0.4"
"@firebase/remote-config-types@0.1.8":
version "0.1.8"
resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.1.8.tgz#0c8d8a839621230053ba55704b5d1145bfe54daa"
integrity sha512-K12IBHO7OD4gCW0FEqZL9zMqVAfS4+joC4YIn3bHezZfu3RL+Bw1wCb0cAD7RfDPcQxWJjxOHpce4YhuqSxPFA==
"@firebase/remote-config@0.1.20":
version "0.1.20"
resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.1.20.tgz#1193be41db9f32a5714a0fdc5f092ef05342f32a"
integrity sha512-7ib4YhhQ/yjEhMiFsYEt4lId+9mzv5CGhGccArmgCyTNSkeImS/BqAeqcOtveyFXHSv9RDHaA4/L6066LsudRQ==
dependencies:
"@firebase/component" "0.1.11"
"@firebase/installations" "0.4.9"
"@firebase/logger" "0.2.3"
"@firebase/remote-config-types" "0.1.8"
"@firebase/util" "0.2.46"
tslib "1.11.1"
"@firebase/storage-types@0.3.11":
version "0.3.11"
resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.3.11.tgz#98f6ced5460502ab12778ce71d4dc9bf0ab7f2ee"
integrity sha512-EMOo5aeiJIa8eQ/VqjIa/DYlDcEJX1V84FOxmLfNWZIlmCSvcqx9E9mcNlOnoUB4iePqQjTMQRtKlIBvvEVhVg==
"@firebase/storage@0.3.33":
version "0.3.33"
resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.3.33.tgz#d531aa01a4e41e0a34c80982bd62ca5a652e31db"
integrity sha512-pFhsy+LglBjyVAYd6LlyUTeHTXR4yV24eL+fLZCYOE3W23Ago/3RpkX+MaEP5ZSpdFmnV/H6R6qDClSFx1EEYA==
dependencies:
"@firebase/component" "0.1.11"
"@firebase/storage-types" "0.3.11"
"@firebase/util" "0.2.46"
tslib "1.11.1"
"@firebase/util@0.2.46":
version "0.2.46"
resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.2.46.tgz#e14e5fda81df8ed22f028d4b4a0e7d646b384331"
integrity sha512-rKzQRc7YAbve+MECliis5ac6lRB1AZgOyZdoAbXaEtmGWUwnlM99uNhCekA963CaBkzlHwQG2inLf3WGW7nLFA==
dependencies:
tslib "1.11.1"
"@firebase/webchannel-wrapper@0.2.40":
version "0.2.40"
resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.40.tgz#5215554d2ce1d87329241e8b08ac96e4dd1994ea"
integrity sha512-f0jc79nQvwcwhOGFAD9b5K55Cb/a0A7LKBdRyQgVFLBGm+MuSFF5Rm/5Ll8/u72hJhbdICQj+xYl2uIuCMdXFQ==
"@fortawesome/fontawesome-common-types@^0.2.28":
version "0.2.28"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz#1091bdfe63b3f139441e9cba27aa022bff97d8b2"
integrity sha512-gtis2/5yLdfI6n0ia0jH7NJs5i/Z/8M/ZbQL6jXQhCthEOe5Cr5NcQPhgTvFxNOtURE03/ZqUcEskdn2M+QaBg==
"@fortawesome/fontawesome-free@^5.13.0":
version "5.13.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9"
integrity sha512-xKOeQEl5O47GPZYIMToj6uuA2syyFlq9EMSl2ui0uytjY9xbe8XS0pexNWmxrdcCyNGyDmLyYw5FtKsalBUeOg==
"@fortawesome/fontawesome-svg-core@^1.2.28":
version "1.2.28"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.28.tgz#e5b8c8814ef375f01f5d7c132d3c3a2f83a3abf9"
integrity sha512-4LeaNHWvrneoU0i8b5RTOJHKx7E+y7jYejplR7uSVB34+mp3Veg7cbKk7NBCLiI4TyoWS1wh9ZdoyLJR8wSAdg==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.28"
"@fortawesome/free-brands-svg-icons@^5.13.0":
version "5.13.0"
resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.13.0.tgz#e79de73ba6555055204828dca9c0691e7ce5242b"
integrity sha512-/6xXiJFCMEQxqxXbL0FPJpwq5Cv6MRrjsbJEmH/t5vOvB4dILDpnY0f7zZSlA8+TG7jwlt12miF/yZpZkykucA==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.28"
"@fortawesome/free-regular-svg-icons@^5.13.0":
version "5.13.0"
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.13.0.tgz#925a13d8bdda0678f71551828cac80ab47b8150c"
integrity sha512-70FAyiS5j+ANYD4dh9NGowTorNDnyvQHHpCM7FpnF7GxtDjBUCKdrFqCPzesEIpNDFNd+La3vex+jDk4nnUfpA==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.28"
"@fortawesome/free-solid-svg-icons@^5.13.0":
version "5.13.0"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.13.0.tgz#44d9118668ad96b4fd5c9434a43efc5903525739"
integrity sha512-IHUgDJdomv6YtG4p3zl1B5wWf9ffinHIvebqQOmV3U+3SLw4fC+LUCCgwfETkbTtjy5/Qws2VoVf6z/ETQpFpg==
dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.28"
"@fortawesome/react-fontawesome@^0.1.9":
version "0.1.9"
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.9.tgz#c865b9286c707407effcec99958043711367cd02"
integrity sha512-49V3WNysLZU5fZ3sqSuys4nGRytsrxJktbv3vuaXkEoxv22C6T7TEG0TW6+nqVjMnkfCQd5xOnmJoZHMF78tOw==
dependencies:
prop-types "^15.7.2"
"@grpc/grpc-js@0.8.1":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-0.8.1.tgz#3003a422577da39e7113566f2fdd4872f31e6090"
integrity sha512-e8gSjRZnOUefsR3obOgxG9RtYW2Mw83hh7ogE2ByCdgRhoX0mdnJwBcZOami3E0l643KCTZvORFwfSEi48KFIQ==
dependencies:
semver "^6.2.0"
"@grpc/proto-loader@^0.5.0":
version "0.5.4"
resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.4.tgz#038a3820540f621eeb1b05d81fbedfb045e14de0"
integrity sha512-HTM4QpI9B2XFkPz7pjwMyMgZchJ93TVkL3kWPW8GDMDKYxsMnmf4w2TNMJK7+KNiYHS5cJrCEAFlF+AwtXWVPA==
dependencies:
lodash.camelcase "^4.3.0"
protobufjs "^6.8.6"
"@hapi/address@2.x.x":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@ -1285,6 +1568,69 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
"@protobufjs/base64@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
"@protobufjs/codegen@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
"@protobufjs/eventemitter@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
"@protobufjs/fetch@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
dependencies:
"@protobufjs/aspromise" "^1.1.1"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/float@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
"@protobufjs/inquire@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
"@protobufjs/path@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
"@protobufjs/pool@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
"@protobufjs/utf8@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
"@reduxjs/toolkit@^1.3.6":
version "1.3.6"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.3.6.tgz#306171fce2ab7423931736c34fa82190959dcd62"
integrity sha512-eNYURfoJa6mRNU5YtBVbmE5+nDoc4lpjZ181PBwRC6nIFYZdNR3GcoQ4uomFt8eHpXAUAdpCdxBlDsmwyXOt9Q==
dependencies:
immer "^6.0.1"
redux "^4.0.0"
redux-thunk "^2.3.0"
reselect "^4.0.0"
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
@ -1527,6 +1873,19 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/history@*":
version "4.7.5"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860"
integrity sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@ -1578,6 +1937,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440"
integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==
"@types/long@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
"@types/minimatch@*", "@types/minimatch@3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@ -1598,6 +1962,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.38.tgz#58841a382f231ad005dbb935c36d44aa1118a26b"
integrity sha512-75eLjX0pFuTcUXnnWmALMzzkYorjND0ezNEycaKesbUBg9eGZp4GHPuDmkRc4mQQvIpe29zrzATNRA6hkYqwmA==
"@types/node@^13.7.0":
version "13.13.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.6.tgz#caa6756b64d30547a2082235531fa0dd8cba1b6e"
integrity sha512-zqRj8ugfROCjXCNbmPBe2mmQ0fJWP9lQaN519hwunOgpHgVykme4G6FW95++dyNFDvJUk4rtExkVkL0eciu5NA==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -1620,6 +1989,33 @@
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.8":
version "7.1.8"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.8.tgz#3631feb559f7858d6ad9eea1d6ef41fa64fe7205"
integrity sha512-kpplH7Wg2SYU00sZVT98WBN0ou6QKrYcShRaW+5Vpe5l7bluKWJbWmAL+ieiso07OQzpcP5i1PeY3690640ZWg==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-router-dom@^5.1.5":
version "5.1.5"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.5.tgz#7c334a2ea785dbad2b2dcdd83d2cf3d9973da090"
integrity sha512-ArBM4B1g3BWLGbaGvwBGO75GNFbLDUthrDojV2vHLih/Tq8M+tgvY1DSwkuNrPSwdp/GUL93WSEpTZs8nVyJLw==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react-router" "*"
"@types/react-router@*":
version "5.1.7"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.7.tgz#e9d12ed7dcfc79187e4d36667745b69a5aa11556"
integrity sha512-2ouP76VQafKjtuc0ShpwUebhHwJo0G6rhahW9Pb8au3tQTjYXd2jta4wv6U2tGLR/I42yuG00+UXjNYY0dTzbg==
dependencies:
"@types/history" "*"
"@types/react" "*"
"@types/react@*", "@types/react@^16.9.0":
version "16.9.35"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368"
@ -3308,6 +3704,11 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
core-js@3.6.5:
version "3.6.5"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
core-js@^2.4.0:
version "2.6.11"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
@ -3944,6 +4345,11 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
dom-storage@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.1.0.tgz#00fb868bc9201357ea243c7bcfd3304c1e34ea39"
integrity sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q==
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@ -4090,6 +4496,13 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
encoding@^0.1.11:
version "0.1.12"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=
dependencies:
iconv-lite "~0.4.13"
end-of-stream@^1.0.0, end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@ -4643,6 +5056,13 @@ fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
faye-websocket@0.11.3, faye-websocket@~0.11.1:
version "0.11.3"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e"
integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==
dependencies:
websocket-driver ">=0.5.1"
faye-websocket@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
@ -4650,13 +5070,6 @@ faye-websocket@^0.10.0:
dependencies:
websocket-driver ">=0.5.1"
faye-websocket@~0.11.1:
version "0.11.3"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e"
integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==
dependencies:
websocket-driver ">=0.5.1"
fb-watchman@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85"
@ -4810,6 +5223,26 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
firebase@^7.14.3:
version "7.14.3"
resolved "https://registry.yarnpkg.com/firebase/-/firebase-7.14.3.tgz#dfe6fa3e5982a6d6d6d44bfc50dba23568a5a777"
integrity sha512-qExwE/vhf/l6Mf8ES3IFX1SB6/DysKPtvrIWnIuswmRMeSA9eERwrqp4Pom4NHhzjBP1jOmlIPKeOplsNwMlOQ==
dependencies:
"@firebase/analytics" "0.3.4"
"@firebase/app" "0.6.3"
"@firebase/app-types" "0.6.0"
"@firebase/auth" "0.14.5"
"@firebase/database" "0.6.2"
"@firebase/firestore" "1.14.3"
"@firebase/functions" "0.4.43"
"@firebase/installations" "0.4.9"
"@firebase/messaging" "0.6.15"
"@firebase/performance" "0.3.3"
"@firebase/polyfill" "0.3.35"
"@firebase/remote-config" "0.1.20"
"@firebase/storage" "0.3.33"
"@firebase/util" "0.2.46"
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
@ -5250,6 +5683,18 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
history@^4.9.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
dependencies:
"@babel/runtime" "^7.1.2"
loose-envify "^1.2.0"
resolve-pathname "^3.0.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
value-equal "^1.0.1"
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -5259,6 +5704,13 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
@ -5418,7 +5870,7 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
iconv-lite@0.4.24, iconv-lite@^0.4.24:
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -5432,6 +5884,11 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
dependencies:
postcss "^7.0.14"
idb@3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/idb/-/idb-3.0.2.tgz#c8e9122d5ddd40f13b60ae665e4862f8b13fa384"
integrity sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==
identity-obj-proxy@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14"
@ -5464,6 +5921,16 @@ immer@1.10.0:
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
immer@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/immer/-/immer-5.0.0.tgz#07f462b7d95f7e86c214a861ecacd766d42b8c0a"
integrity sha512-G7gRqKbi9NE025XVyqyTV98dxUOtdKvu/P1QRaVZfA55aEcXgjbxPdm+TlWdcSMNPKijlaHNz61DGPyelouRlA==
immer@^6.0.1:
version "6.0.5"
resolved "https://registry.yarnpkg.com/immer/-/immer-6.0.5.tgz#77187d13b71c6cee40dde3b8e87a50a7a636d630"
integrity sha512-Q2wd90qrgFieIpLzAO2q9NLEdmyp/sr76Ml4Vm5peUKgyTa2CQa3ey8zuzwSKOlKH7grCeGBGUcLLVCVW1aguA==
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@ -5921,7 +6388,7 @@ is-root@2.1.0:
resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c"
integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==
is-stream@^1.1.0:
is-stream@^1.0.1, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
@ -5965,6 +6432,11 @@ is-wsl@^2.1.1:
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.1.1.tgz#4a1c152d429df3d441669498e2486d3596ebaf1d"
integrity sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@ -5987,6 +6459,14 @@ isobject@^3.0.0, isobject@^3.0.1:
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
isomorphic-fetch@2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
dependencies:
node-fetch "^1.0.1"
whatwg-fetch ">=0.10.0"
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@ -6826,6 +7306,11 @@ lodash._reinterpolate@^3.0.0:
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@ -6894,7 +7379,12 @@ loglevel@^1.6.6:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.7.tgz#b3e034233188c68b889f5b862415306f565e2c56"
integrity sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
long@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -7103,6 +7593,14 @@ min-indent@^1.0.0:
resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256"
integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=
mini-create-react-context@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz#df60501c83151db69e28eac0ef08b4002efab040"
integrity sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==
dependencies:
"@babel/runtime" "^7.5.5"
tiny-warning "^1.0.3"
mini-css-extract-plugin@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e"
@ -7226,6 +7724,11 @@ moment@2.24.0:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
moment@^2.25.3:
version "2.25.3"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.25.3.tgz#252ff41319cf41e47761a1a88cab30edfe9808c0"
integrity sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg==
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@ -7326,6 +7829,14 @@ no-case@^3.0.3:
lower-case "^2.0.1"
tslib "^1.10.0"
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
dependencies:
encoding "^0.1.11"
is-stream "^1.0.1"
node-forge@0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
@ -7916,6 +8427,13 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
@ -8785,6 +9303,11 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
promise-polyfill@8.1.3:
version "8.1.3"
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116"
integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g==
promise@^8.0.3:
version "8.1.0"
resolved "https://registry.yarnpkg.com/promise/-/promise-8.1.0.tgz#697c25c3dfe7435dd79fcd58c38a135888eaf05e"
@ -8809,6 +9332,25 @@ prop-types@^15.6.2, prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
protobufjs@^6.8.6:
version "6.9.0"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.9.0.tgz#c08b2bf636682598e6fabbf0edb0b1256ff090bd"
integrity sha512-LlGVfEWDXoI/STstRDdZZKb/qusoAWUnmLg9R8OLSO473mBLWHowx8clbX5/+mKDEI+v7GzjoK9tRPZMMcoTrg==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
"@protobufjs/codegen" "^2.0.4"
"@protobufjs/eventemitter" "^1.1.0"
"@protobufjs/fetch" "^1.1.0"
"@protobufjs/float" "^1.0.2"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/path" "^1.1.2"
"@protobufjs/pool" "^1.1.0"
"@protobufjs/utf8" "^1.1.0"
"@types/long" "^4.0.1"
"@types/node" "^13.7.0"
long "^4.0.0"
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
@ -9016,11 +9558,67 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
react-is@^16.12.0, react-is@^16.8.1, react-is@^16.8.4:
react-google-button@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/react-google-button/-/react-google-button-0.7.1.tgz#e84b4ce270a66e345489dc86e47235e877fcc81a"
integrity sha512-FN8/9Va6oAGKL561yddNzcjz0iGlt2GZFwiDPczEu0fDVnR21/nBrPs7Y5x97V1S4//nvtkvv6ohfwtxIHpbfg==
dependencies:
prop-types "^15.7.2"
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-redux-firebase@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/react-redux-firebase/-/react-redux-firebase-3.4.0.tgz#acf317a3c3912ae2eb5a25aa252087ac5ca6abe2"
integrity sha512-wT+39Qsb7D5CIVzaPHxbSq9ti4FkdDpXp79ZpESS1cq8NQOywkFnybi1PVUD3Owj9yz1SeiclxIXPejqSZ9SGw==
dependencies:
hoist-non-react-statics "^3.3.2"
lodash "^4.17.15"
prop-types "^15.7.2"
react-redux@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
dependencies:
"@babel/runtime" "^7.5.5"
hoist-non-react-statics "^3.3.0"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^16.9.0"
react-router-dom@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662"
integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
loose-envify "^1.3.1"
prop-types "^15.6.2"
react-router "5.2.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293"
integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==
dependencies:
"@babel/runtime" "^7.1.2"
history "^4.9.0"
hoist-non-react-statics "^3.1.0"
loose-envify "^1.3.1"
mini-create-react-context "^0.4.0"
path-to-regexp "^1.7.0"
prop-types "^15.6.2"
react-is "^16.6.0"
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-scripts@3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.4.1.tgz#f551298b5c71985cc491b9acf3c8e8c0ae3ada0a"
@ -9184,6 +9782,33 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
reduce-reducers@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reduce-reducers/-/reduce-reducers-1.0.4.tgz#fb77e751a9eb0201760ac5a605ca8c9c2d0537f8"
integrity sha512-Mb2WZ2bJF597exiqX7owBzrqJ74DHLK3yOQjCyPAaNifRncE8OD0wFIuoMhXxTnHK07+8zZ2SJEKy/qtiyR7vw==
redux-firestore@^0.13.0:
version "0.13.0"
resolved "https://registry.yarnpkg.com/redux-firestore/-/redux-firestore-0.13.0.tgz#154e705726740c6173e2f8a1d82214aeccc6954f"
integrity sha512-0Vf/MXrH6gUqVwSmAQAz9Ic1J4uADCaF29DcpcNU2ecZoYG5aQSZO2iGXUqzjAOeatE1Ksz8SDEvKu8gBRvYXg==
dependencies:
immer "5.0.0"
lodash "^4.17.15"
reduce-reducers "^1.0.4"
redux-thunk@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
redux@^4.0.0:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@ -9369,6 +9994,11 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
reselect@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@ -9386,6 +10016,11 @@ resolve-from@^4.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
resolve-pathname@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
resolve-url-loader@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.1.tgz#28931895fa1eab9be0647d3b2958c100ae3c0bf0"
@ -10324,7 +10959,7 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1"
util.promisify "~1.0.0"
symbol-observable@^1.1.0:
symbol-observable@^1.1.0, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@ -10443,6 +11078,16 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-invariant@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tiny-warning@^1.0.0, tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tmp@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
@ -10529,7 +11174,7 @@ ts-pnp@1.1.6, ts-pnp@^1.1.6:
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.6.tgz#389a24396d425a0d3162e96d2b4638900fdc289a"
integrity sha512-CrG5GqAAzMT7144Cl+UIFP7mz/iIhiy+xQ6GGcnjTezhALT02uPMRw7tgDSESgB5MsfKt55+GPWw4ir1kVtMIQ==
tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0:
tslib@1.11.1, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
@ -10598,10 +11243,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@~3.7.2:
version "3.7.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
typescript@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9"
integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
@ -10800,6 +11445,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
value-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@ -10999,7 +11649,12 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
dependencies:
iconv-lite "0.4.24"
whatwg-fetch@^3.0.0:
whatwg-fetch@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"
integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==
whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
@ -11270,6 +11925,11 @@ xmlchars@^2.1.1:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmlhttprequest@1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=
xregexp@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.3.0.tgz#7e92e73d9174a99a59743f67a4ce879a04b5ae50"