{
13 | constructor(props: Props) {
14 | super(props);
15 | this.state = { error: null, info: null };
16 | }
17 |
18 | componentDidCatch = (error: Error, info: ErrorInfo) => {
19 | this.setState({
20 | error,
21 | info,
22 | });
23 | };
24 |
25 | render() {
26 | const { info, error } = this.state;
27 | const { children } = this.props;
28 | return (
29 | info ? (
30 |
31 |
Something went seriouosly wrong.
32 |
33 | {error && error.toString()}
34 |
35 | {info!.componentStack}
36 |
37 |
38 | ) : (children)
39 | );
40 | }
41 | }
42 |
43 | export default ErrorBoundary;
44 |
--------------------------------------------------------------------------------
/src/components/common/CustomParticles/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Particles from 'react-particles-js';
3 | import { useThemeMode, computeTheme } from '../../../theme';
4 | import useWindowDimensions from '../../../utils/window-size';
5 |
6 | function CustomParticles() {
7 | const currentTheme = useThemeMode();
8 | const theme = computeTheme(currentTheme);
9 |
10 | const { height } = useWindowDimensions();
11 | let numParticles = 120;
12 | if (height < 800) {
13 | numParticles = 20;
14 | }
15 |
16 | return (
17 |
35 | );
36 | }
37 |
38 | export default CustomParticles;
39 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import { Provider } from 'react-redux';
5 | import { BrowserRouter as Router } from 'react-router-dom';
6 |
7 | import App from './App';
8 | import store from './app/store';
9 | import * as serviceWorker from './serviceWorker';
10 | import { isLoggedIn } from './client/jwt';
11 | import { loginSuccess } from './app/modular/auth/actions';
12 | import { getCurrentUser } from './client/user';
13 |
14 | const user = getCurrentUser();
15 | if (isLoggedIn() && user) {
16 | user.updatedAt = new Date(user.updatedAt);
17 | store.dispatch(loginSuccess(
18 | user,
19 | ));
20 | }
21 |
22 | ReactDOM.render(
23 |
24 |
25 |
26 |
27 |
28 |
29 | ,
30 | document.getElementById('root'),
31 | );
32 |
33 | // If you want your app to work offline and load faster, you can change
34 | // unregister() to register() below. Note this comes with some pitfalls.
35 | // Learn more about service workers: https://bit.ly/CRA-PWA
36 | serviceWorker.unregister();
37 |
--------------------------------------------------------------------------------
/src/app/modular/application/actions.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | import { action } from 'typesafe-actions';
3 | import { ThunkAction } from 'redux-thunk';
4 | import { RootState } from '../../types';
5 | import { Applications } from './types';
6 | import * as request from '../../../client';
7 |
8 | export const SAVE_APPLICATIONS = 'applications/saveApplications';
9 |
10 | export const saveApplications = (applications: Applications) => (
11 | action(SAVE_APPLICATIONS, { applications })
12 | );
13 |
14 | export const getApplications = ():
15 | ThunkAction => (async (dispatch) => {
16 | try {
17 | const res = await request.get({
18 | url: 'applications',
19 | });
20 | const applications: Applications = {};
21 | for (const application of res) {
22 | application.applicant.updatedAt = new Date(application.applicant.updatedAt);
23 | application.appliedAt = new Date(application.appliedAt);
24 | applications[application.id] = application;
25 | }
26 | dispatch(saveApplications(applications));
27 | } catch (err) {
28 | // eslint-disable-next-line no-console
29 | console.error(err);
30 | }
31 | });
32 |
--------------------------------------------------------------------------------
/src/components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Typography, Link } from '@material-ui/core';
3 | import { makeStyles } from '@material-ui/core/styles';
4 |
5 | function FooterInfo() {
6 | return (
7 |
8 |
9 | Talent
10 |
11 | {' '}
12 | {new Date().getFullYear()}
13 | .
14 |
15 | );
16 | }
17 |
18 | const useStyles = makeStyles((theme) => ({
19 | footer: {
20 | padding: theme.spacing(2, 3),
21 | position: 'fixed',
22 | bottom: '0',
23 | left: '0',
24 | width: '100%',
25 | backgroundColor:
26 | theme.palette.type === 'light'
27 | ? theme.palette.grey[200]
28 | : theme.palette.grey[800],
29 | },
30 | }));
31 |
32 | export default function Footer() {
33 | const classes = useStyles();
34 |
35 | return (
36 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/Modals/AuthLoadingModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Typography, LinearProgress } from '@material-ui/core';
3 | import { makeStyles } from '@material-ui/core/styles';
4 |
5 | import { connect } from 'react-redux';
6 | import Container from '../container';
7 | import { RootState } from '../../../app/types';
8 |
9 | import authDuck from '../../../app/modular/auth';
10 |
11 | interface ConnectedProps {
12 | loading: boolean;
13 | }
14 |
15 | type Props = ConnectedProps;
16 |
17 | const useStyles = makeStyles((theme) => ({
18 | paper: {
19 | outline: 'none',
20 | },
21 | linearProgress: {
22 | width: '100%',
23 | marginTop: theme.spacing(1),
24 | },
25 | loading: {
26 | marginTop: theme.spacing(1),
27 | },
28 | }));
29 |
30 | const AuthLoadingModal: React.FC = ({
31 | loading,
32 | }: Props) => {
33 | const classes = useStyles();
34 | return (
35 |
39 |
40 |
41 | Loading...
42 |
43 |
44 | );
45 | };
46 |
47 | const mapStateToProps = (state: RootState): Props => ({
48 | loading: authDuck.selectors.loading(state),
49 | });
50 |
51 | export default connect(mapStateToProps)(AuthLoadingModal);
52 |
--------------------------------------------------------------------------------
/src/components/common/PostCard/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CssBaseline,
3 | Typography,
4 | Button,
5 | Grid,
6 | } from '@material-ui/core';
7 | import React from 'react';
8 | import { useHistory } from 'react-router-dom';
9 | import { Post } from '../../../app/modular/post/types';
10 |
11 | interface Props {
12 | post: Post
13 | }
14 |
15 | function PostCard(props: Props) {
16 | const { post } = props;
17 | const { title, author } = post;
18 | const history = useHistory();
19 |
20 | const handleClick = () => {
21 | history.push(`/post/${post.id}`);
22 | };
23 |
24 | return (
25 |
26 |
27 |
28 |
29 | {title}
30 |
31 | {`${author.firstName} ${author.lastName}`}
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | export default PostCard;
46 |
--------------------------------------------------------------------------------
/src/utils/relative-time.ts:
--------------------------------------------------------------------------------
1 | function calculateRelativeTime(prevTime: Date): string {
2 | const msPerMinute = 60 * 1000;
3 | const msPerHour = msPerMinute * 60;
4 | const msPerDay = msPerHour * 24;
5 | const msPerMonth = msPerDay * 30;
6 | const msPerYear = msPerDay * 365;
7 |
8 | const difference = new Date().getTime() - prevTime.getTime();
9 |
10 | let relativeTime;
11 | if (difference < msPerMinute) {
12 | relativeTime = `${Math.round(difference / 1000)} seconds ago`;
13 | } else if (difference < msPerHour) {
14 | relativeTime = `${Math.round(difference / msPerMinute)} minutes ago`;
15 | } else if (difference < msPerDay) {
16 | relativeTime = `${Math.round(difference / msPerHour)} hours ago`;
17 | } else if (difference < msPerMonth) {
18 | relativeTime = `${Math.round(difference / msPerDay)} days ago`;
19 | } else if (difference < msPerYear) {
20 | relativeTime = `${Math.round(difference / msPerMonth)} months ago`;
21 | } else {
22 | relativeTime = `${Math.round(difference / msPerYear)} years ago`;
23 | }
24 |
25 | // using slicing to remove the plural if the number is 1
26 | if (relativeTime.charAt(0) === '1' && relativeTime.charAt(1) === ' ') {
27 | relativeTime = relativeTime.slice(0, relativeTime.length - 5)
28 | + relativeTime.slice(-4, relativeTime.length);
29 | }
30 |
31 | return relativeTime;
32 | }
33 |
34 | export default calculateRelativeTime;
35 |
--------------------------------------------------------------------------------
/src/components/Modals/container.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | makeStyles, Modal, Backdrop, Fade, Paper,
4 | } from '@material-ui/core';
5 |
6 | export const useStyles = makeStyles((theme) => ({
7 | modal: {
8 | display: 'flex',
9 | alignItems: 'center',
10 | justifyContent: 'center',
11 | },
12 | paper: {
13 | backgroundColor: theme.palette.background.paper,
14 | boxShadow: theme.shadows[8],
15 | padding: theme.spacing(2, 4, 3),
16 | },
17 | }));
18 |
19 | interface Props {
20 | handleClose?: VoidFunction;
21 | children: React.ReactNode;
22 | open: boolean;
23 | className?: string;
24 | }
25 |
26 | const Container: React.FC = ({
27 | handleClose,
28 | children,
29 | open,
30 | className,
31 | }: Props) => {
32 | const classes = useStyles();
33 |
34 | return (
35 |
47 |
50 |
51 | {children}
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default Container;
59 |
--------------------------------------------------------------------------------
/src/app/modular/post/reducer.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import produce from 'immer';
3 | import type { Reducer } from 'redux';
4 | import { ActionType } from 'typesafe-actions';
5 |
6 | import type { PostState } from './types';
7 | import * as actions from './actions';
8 |
9 | type PostAction = ActionType;
10 |
11 | const initialState: PostState = {
12 | loading: false,
13 | posts: {},
14 | };
15 |
16 | export type PostReducer = Reducer;
17 |
18 | const reducer: PostReducer = produce(
19 | (state: PostState, action: PostAction) => {
20 | switch (action.type) {
21 | case actions.LOADING_START:
22 | state.loading = true;
23 | break;
24 | case actions.SAVE_POSTS:
25 | state.posts = action.payload.posts;
26 | break;
27 | case actions.ADD_POST:
28 | // eslint-disable-next-line no-case-declarations
29 | const { post } = action.payload;
30 | state.posts[post.id] = post;
31 | state.loading = false;
32 | break;
33 | case actions.LOADING_SUCCESS:
34 | state.loading = false;
35 | break;
36 | case actions.LOADING_ERROR:
37 | state.loading = false;
38 | switch (action.payload.error.slice(-3)) {
39 | case '401':
40 | state.error = 'Unauthorized';
41 | break;
42 | default:
43 | state.error = action.payload.error;
44 | break;
45 | }
46 | break;
47 | case actions.CLEAR_ERROR:
48 | state.error = undefined;
49 | break;
50 | default:
51 | break;
52 | }
53 | },
54 | initialState,
55 | );
56 |
57 | export default reducer;
58 |
--------------------------------------------------------------------------------
/src/theme.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { createMuiTheme, Theme } from '@material-ui/core';
3 |
4 | export const useThemeMode = () => {
5 | const localTheme = localStorage.getItem('themeType');
6 | const [currentTheme, setCurrentTheme] = useState<'light' | 'dark' | undefined>(
7 | localTheme === 'light' || localTheme === 'dark' ? localTheme : 'light',
8 | );
9 |
10 | const setTheme = () => {
11 | const theme = localStorage.getItem('themeType');
12 | if (theme === 'light' || theme === 'dark') {
13 | setCurrentTheme(theme ?? undefined);
14 | }
15 | };
16 |
17 | useEffect(() => {
18 | window.addEventListener('storage themeType', () => {
19 | setTheme();
20 | }, false);
21 | if (!localStorage.getItem('themeType')) {
22 | localStorage.setItem('themeType', 'light');
23 | }
24 | });
25 |
26 | const { setItem } = localStorage;
27 | // eslint-disable-next-line func-names
28 | localStorage.setItem = function (key, value) {
29 | setItem.apply(this, [key, value]);
30 | const e = new Event(`storage ${key}`);
31 | window.dispatchEvent(e);
32 | };
33 |
34 | return currentTheme;
35 | };
36 |
37 | export const computeTheme = (currentTheme: 'light' | 'dark' | undefined): Theme => {
38 | const light = currentTheme === 'light';
39 | return createMuiTheme({
40 | palette: {
41 | type: currentTheme,
42 | primary: {
43 | main: light ? '#1976d2' : '#f50057',
44 | },
45 | background: {
46 | default: light ? '#fafafa' : '#121212',
47 | paper: light ? '#fff' : '#1c1c1c',
48 | },
49 | error: {
50 | main: light ? '#B00020' : '#CF6679',
51 | contrastText: light ? '#FFFFFF' : '#121212',
52 | },
53 | },
54 | });
55 | };
56 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useEffect } from 'react';
2 | import { Route, Switch, Redirect } from 'react-router-dom';
3 | import { ThemeProvider, Theme } from '@material-ui/core';
4 | import './App.css';
5 | import { useDispatch } from 'react-redux';
6 | import Login from './pages/Login';
7 | import Signup from './pages/Signup';
8 | import Landing from './pages/Landing';
9 | import { isLoggedIn } from './client/jwt';
10 | import Modals from './components/Modals';
11 | import Header from './components/Header';
12 | import Footer from './components/Footer';
13 | import { useThemeMode, computeTheme } from './theme';
14 | import JobDetails from './components/JobDetails';
15 | import { getPosts } from './app/modular/post/actions';
16 |
17 | function App() {
18 | const dispatch = useDispatch();
19 | useEffect(() => {
20 | dispatch(getPosts());
21 | });
22 | const currentTheme = useThemeMode();
23 |
24 | const theme: Theme = useMemo(
25 | () => computeTheme(currentTheme),
26 | [currentTheme],
27 | );
28 |
29 | return (
30 |
31 |
32 |
33 | (isLoggedIn() ? : )} />
34 | (isLoggedIn() ? : )} />
35 | ()} />
36 | }
46 | />
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | export default App;
55 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
28 |
29 | React Redux App
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/app/modular/auth/reducer.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | import produce from 'immer';
3 | import type { Reducer } from 'redux';
4 | import { ActionType } from 'typesafe-actions';
5 |
6 | import type { AuthState } from './types';
7 | import * as actions from './actions';
8 |
9 | import { JWT_ACCESS_KEY } from '../../../client/jwt';
10 |
11 | type AuthActions = ActionType;
12 |
13 | const initialState: AuthState = {
14 | loading: false,
15 | loggedIn: localStorage.getItem(JWT_ACCESS_KEY) !== null,
16 | };
17 | // change this in the future to check for valid token instead of just existence
18 |
19 | export type AuthReducer = Reducer;
20 |
21 | const reducer: AuthReducer = produce(
22 | (state: AuthState, action: AuthActions) => {
23 | switch (action.type) {
24 | case actions.LOGIN_START:
25 | case actions.SIGNUP_START:
26 | state.loading = true;
27 | break;
28 | case actions.LOGIN_ERROR:
29 | state.loading = false;
30 | switch (action.payload.error.slice(-3)) {
31 | case '401':
32 | state.error = 'Incorrect credentials.';
33 | break;
34 | default:
35 | state.error = action.payload.error;
36 | break;
37 | }
38 | break;
39 | case actions.LOGOUT_SUCCESS:
40 | state.loggedIn = false;
41 | break;
42 | case actions.SIGNUP_ERROR:
43 | state.loading = false;
44 | switch (action.payload.error.slice(-3)) {
45 | case '409':
46 | state.error = 'Credentials in use.';
47 | break;
48 | default:
49 | state.error = action.payload.error;
50 | break;
51 | }
52 | break;
53 | case actions.LOGIN_SUCCESS:
54 | state.loggedIn = true;
55 | break;
56 | case actions.SIGNUP_SUCCESS:
57 | state.currentUser = action.payload.user;
58 | break;
59 | case actions.CLEAR_ERROR:
60 | state.error = undefined;
61 | break;
62 | default:
63 | break;
64 | }
65 | },
66 | initialState,
67 | );
68 |
69 | export default reducer;
70 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "talent-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.11.0",
7 | "@material-ui/icons": "^4.9.1",
8 | "@material-ui/lab": "^4.0.0-alpha.57",
9 | "@reduxjs/toolkit": "^1.2.5",
10 | "@testing-library/jest-dom": "^4.2.4",
11 | "@testing-library/react": "^9.3.2",
12 | "@testing-library/user-event": "^7.1.2",
13 | "@types/jest": "^24.0.0",
14 | "@types/node": "^12.0.0",
15 | "@types/react": "^16.9.0",
16 | "@types/react-dom": "^16.9.0",
17 | "@types/react-redux": "^7.1.7",
18 | "@types/yup": "^0.29.3",
19 | "axios": "^0.21.1",
20 | "connected-react-router": "^6.8.0",
21 | "formik": "^2.1.5",
22 | "formik-material-ui": "^2.0.1",
23 | "history": "^5.0.0",
24 | "immer": "^7.0.5",
25 | "react": "^16.13.1",
26 | "react-dom": "^16.13.1",
27 | "react-particles-js": "^3.4.1",
28 | "react-redux": "^7.2.0",
29 | "react-router": "^5.2.0",
30 | "react-router-dom": "^5.2.0",
31 | "react-scripts": "3.4.1",
32 | "redux": "^4.0.5",
33 | "redux-thunk": "^2.3.0",
34 | "typesafe-actions": "^5.1.0",
35 | "typescript": "~3.8.2",
36 | "yup": "^0.29.1"
37 | },
38 | "scripts": {
39 | "start": "react-scripts start",
40 | "lint": "eslint ./src --ext .js,.ts,.tsx",
41 | "types": "tsc --noEmit",
42 | "build": "react-scripts build",
43 | "test": "react-scripts test",
44 | "eject": "react-scripts eject"
45 | },
46 | "eslintConfig": {
47 | "extends": "react-app"
48 | },
49 | "browserslist": {
50 | "production": [
51 | ">0.2%",
52 | "not dead",
53 | "not op_mini all"
54 | ],
55 | "development": [
56 | "last 1 chrome version",
57 | "last 1 firefox version",
58 | "last 1 safari version"
59 | ]
60 | },
61 | "devDependencies": {
62 | "@types/history": "^4.7.6",
63 | "@types/react-router": "^5.1.8",
64 | "@types/react-router-dom": "^5.1.5",
65 | "@typescript-eslint/eslint-plugin": "3.1.0",
66 | "eslint-config-airbnb-typescript": "^8.0.2",
67 | "eslint-plugin-import": "2.20.1",
68 | "eslint-plugin-jsx-a11y": "6.2.3",
69 | "eslint-plugin-react": "7.19.0",
70 | "eslint-plugin-react-hooks": "2.5.0"
71 | },
72 | "proxy": "http://localhost:8080/"
73 | }
74 |
--------------------------------------------------------------------------------
/src/pages/Landing/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { compose } from 'redux';
3 | import { connect } from 'react-redux';
4 | import {
5 | Container,
6 | CssBaseline,
7 | makeStyles,
8 | Typography,
9 | Grid,
10 | } from '@material-ui/core';
11 | import { Pagination } from '@material-ui/lab';
12 | import useWindowDimensions from '../../utils/window-size';
13 | import { RootState } from '../../app/types';
14 | import { Post } from '../../app/modular/post/types';
15 | import PostCard from '../../components/common/PostCard';
16 | import CustomParticles from '../../components/common/CustomParticles';
17 |
18 | import postDuck from '../../app/modular/post';
19 |
20 | const useStyles = makeStyles(() => ({
21 | pagination: {
22 | width: '100%',
23 | display: 'flex',
24 | justifyContent: 'center',
25 | position: 'fixed',
26 | bottom: '110px',
27 | },
28 | }));
29 |
30 | interface ConnectedProps {
31 | posts: Post[]
32 | }
33 |
34 | type Props = ConnectedProps;
35 |
36 | const Landing: React.FC = ({ posts }: Props) => {
37 | const classes = useStyles();
38 | const { height } = useWindowDimensions();
39 | let itemsPerPage = 7;
40 | if (height < 800) {
41 | itemsPerPage = 4;
42 | }
43 | const [page, setPage] = useState(1);
44 | const numOfPages = Math.ceil(posts.length / itemsPerPage);
45 |
46 | const handleChange = (event: any, value: number) => {
47 | setPage(value);
48 | };
49 |
50 | return (
51 |
52 |
53 |
54 |
55 |
56 | Open Positions
57 |
58 | {posts.slice((page - 1) * itemsPerPage, page * itemsPerPage).map((post) => (
59 | post.active ? :
60 | ))}
61 |
62 |
63 |
72 |
73 | );
74 | };
75 |
76 | const mapStateToProps = (state: RootState): Props => ({
77 | posts: postDuck.selectors.posts(state),
78 | });
79 |
80 | export default compose(connect(mapStateToProps, null))(Landing);
81 |
--------------------------------------------------------------------------------
/src/app/modular/post/actions.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | import { action } from 'typesafe-actions';
3 | import { ThunkAction } from 'redux-thunk';
4 | import { RootState } from '../../types';
5 | import { Posts, Post } from './types';
6 | import * as request from '../../../client';
7 |
8 | export const LOADING_START = 'post/loadingStart';
9 | export const SAVE_POSTS = 'post/savePosts';
10 | export const LOADING_SUCCESS = 'post/loadingSuccess';
11 | export const LOADING_ERROR = 'post/loadingError';
12 | export const ADD_POST = 'post/addPost';
13 | export const CLEAR_ERROR = 'post/clearError';
14 |
15 | export const loadingStart = () => action(LOADING_START);
16 |
17 | export const savePosts = (posts: Posts) => action(SAVE_POSTS, { posts });
18 |
19 | export const loadingError = (error: string) => action(LOADING_ERROR, { error });
20 |
21 | export const loadingSuccess = () => action(LOADING_SUCCESS);
22 |
23 | export const addPost = (post: Post) => action(ADD_POST, { post });
24 |
25 | export const clearError = () => action(CLEAR_ERROR);
26 |
27 | export const getPosts = ():
28 | ThunkAction => (async (dispatch) => {
29 | try {
30 | const res = await request.unauthenticatedRequest({
31 | method: 'GET',
32 | url: 'posts',
33 | });
34 | const posts : Posts = {};
35 | for (const post of res) {
36 | post.updatedAt = new Date(post.updatedAt);
37 | post.createdAt = new Date(post.createdAt);
38 | post.expiresAt = new Date(post.expiresAt);
39 | post.author.updatedAt = new Date(post.author.updatedAt);
40 | posts[post.id] = post;
41 | }
42 | dispatch(savePosts(posts));
43 | } catch (err) {
44 | // eslint-disable-next-line no-console
45 | console.error(err);
46 | }
47 | });
48 |
49 | export const createPost = ({
50 | title,
51 | description,
52 | desirements,
53 | requirements,
54 | }: {
55 | title: string,
56 | description: string,
57 | desirements: string[],
58 | requirements: string[],
59 | }): ThunkAction => (async (dispatch) => {
60 | dispatch(loadingStart());
61 | try {
62 | const res: any = await request.post({
63 | url: 'posts',
64 | body: {
65 | title,
66 | description,
67 | desirements,
68 | requirements,
69 | },
70 | });
71 | res.updatedAt = new Date(res.updatedAt);
72 | res.createdAt = new Date(res.createdAt);
73 | res.expiresAt = new Date(res.expiresAt);
74 | res.author.updatedAt = new Date(res.author.updatedAt);
75 | dispatch(addPost(res));
76 | } catch (err) {
77 | // eslint-disable-next-line no-console
78 | console.error(err);
79 | dispatch(loadingError(err.message as string));
80 | }
81 | });
82 |
--------------------------------------------------------------------------------
/src/components/JobDetails/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | Container,
5 | Button,
6 | Typography,
7 | Grid,
8 | CssBaseline,
9 | } from '@material-ui/core';
10 | import { connect } from 'react-redux';
11 | import postDuck from '../../app/modular/post';
12 | import { RootState } from '../../app/types';
13 | import { Post } from '../../app/modular/post/types';
14 |
15 | import calculateRelativeTime from '../../utils/relative-time';
16 |
17 | interface ConnectedProps {
18 | post?: Post
19 | }
20 |
21 | interface UnconnectedProps {
22 | id: number
23 | }
24 |
25 | type Props = ConnectedProps & UnconnectedProps;
26 |
27 | const JobDetails: React.FC = ({
28 | post,
29 | }: Props) => {
30 | if (!post) {
31 | return <>404>;
32 | }
33 | const {
34 | title, description, desirements, requirements, createdAt,
35 | } = post!;
36 |
37 | return (
38 |
39 |
40 | {/* HEADER */}
41 |
42 |
43 |
44 | {title}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | {calculateRelativeTime(createdAt)}
53 |
54 |
55 |
56 |
57 | {/* BODY */}
58 |
59 |
60 | About the job
61 |
62 | {description}
63 |
64 |
65 | Required Skills:
66 |
67 | {requirements.map((item: string) => (
68 | - {item}
69 | ))}
70 |
71 |
72 |
73 | Desired Skills:
74 |
75 | {desirements.map((item: string) => (
76 | - {item}
77 | ))}
78 |
79 |
80 |
81 |
82 | );
83 | };
84 |
85 | const mapStateToProps = (state: RootState, { id }: Props): ConnectedProps => ({
86 | post: postDuck.selectors.post(state, id),
87 | });
88 |
89 | export default connect(mapStateToProps)(JobDetails);
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template.
2 |
3 | See backend repo [here](https://github.com/H-Richard/talent-backend).
4 |
5 | ## Prerequisites
6 |
7 | - [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
8 | - [Yarn](https://yarnpkg.com/getting-started/install)
9 |
10 | ## Development
11 |
12 | Git has autocrlf setup by default, once we clone the repo we can disable it with `git config --local core.autocrlf false`
13 |
14 | Then to undo changes run `git rm --cached -r . && git reset --hard`
15 |
16 | To get started with development run `yarn install && yarn start`
17 |
18 |
19 | ## Available Scripts
20 |
21 | In the project directory, you can run:
22 |
23 | ### `yarn start`
24 |
25 | Runs the app in the development mode.
26 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
27 |
28 | The page will reload if you make edits.
29 | You will also see any lint errors in the console.
30 |
31 | ### `yarn test`
32 |
33 | Launches the test runner in the interactive watch mode.
34 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
35 |
36 | ### `yarn build`
37 |
38 | Builds the app for production to the `build` folder.
39 | It correctly bundles React in production mode and optimizes the build for the best performance.
40 |
41 | The build is minified and the filenames include the hashes.
42 | Your app is ready to be deployed!
43 |
44 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
45 |
46 | ### `yarn eject`
47 |
48 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
49 |
50 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
51 |
52 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
53 |
54 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
55 |
56 | ## Learn More
57 |
58 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
59 |
60 | To learn React, check out the [React documentation](https://reactjs.org/).
61 |
--------------------------------------------------------------------------------
/src/components/Modals/AuthErrorModal/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Typography, Icon, Paper } from '@material-ui/core';
3 | import { makeStyles } from '@material-ui/core/styles';
4 | import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined';
5 |
6 | import { connect } from 'react-redux';
7 | import Container from '../container';
8 | import { RootState } from '../../../app/types';
9 |
10 | import authDuck from '../../../app/modular/auth';
11 |
12 | interface ConnectedProps {
13 | error?: string;
14 | }
15 |
16 | interface ConnectedActions {
17 | clearError: typeof authDuck.actions.clearError;
18 | }
19 |
20 | type Props = ConnectedProps & ConnectedActions;
21 |
22 | const useStyles = makeStyles((theme) => ({
23 | paper: {
24 | minHeight: 200,
25 | maxHeight: 800,
26 | minWidth: 300,
27 | maxWidth: 800,
28 | padding: 0,
29 | outline: 'none',
30 | },
31 | errorImageContainer: {
32 | minHeight: 90,
33 | maxHeight: 400,
34 | backgroundColor:
35 | theme.palette.error.main,
36 | borderRadius: '4px 4px 0 0',
37 | textAlign: 'center',
38 | },
39 | errorMessageContainer: {
40 | minHeight: 110,
41 | maxHeight: 400,
42 | backgroundColor:
43 | theme.palette.error.contrastText,
44 | borderRadius: '0 0 4px 4px',
45 | textAlign: 'center',
46 | padding: 10,
47 | },
48 | closeIcon: {
49 | float: 'right',
50 | verticalAlign: 'top',
51 | fontSize: '15px',
52 | color:
53 | theme.palette.background.default,
54 | margin: '8px 8px',
55 | },
56 | errorIcon: {
57 | color:
58 | theme.palette.error.contrastText,
59 | fontSize: '70px',
60 | marginTop: 10,
61 | marginLeft: 30,
62 | },
63 | }));
64 |
65 | const AuthErrorModal: React.FC = ({
66 | error, clearError,
67 | }: Props) => {
68 | const classes = useStyles();
69 |
70 | return (
71 |
76 |
77 |
78 | close
79 |
80 |
81 |
82 | Oops!
83 |
84 |
85 | {error}
86 |
87 |
88 |
89 | );
90 | };
91 |
92 | const mapStateToProps = (state: RootState): ConnectedProps => ({
93 | error: authDuck.selectors.error(state),
94 | });
95 |
96 | const mapDispatchToProps: ConnectedActions = {
97 | clearError: authDuck.actions.clearError,
98 | };
99 |
100 | export default connect(mapStateToProps, mapDispatchToProps)(AuthErrorModal);
101 |
--------------------------------------------------------------------------------
/src/client/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getEncodedAccessToken,
3 | isLoggedIn,
4 | } from './jwt';
5 |
6 | class RequestError extends Error {
7 | response: Response;
8 |
9 | constructor(response: Response) {
10 | super(`Request failure. Status code: ${response.status}`);
11 | this.response = response;
12 | }
13 | }
14 |
15 | export const buildURL = (path: string): string => `http://localhost:8080/${path}`;
16 |
17 | export const getAuthHeader = (): string => `Bearer ${getEncodedAccessToken()}`;
18 |
19 | export const getDefaultHeaders = () => {
20 | const headers: { [key: string]: string } = {
21 | 'Content-Type': 'application/json',
22 | };
23 |
24 | if (isLoggedIn()) {
25 | headers.Authorization = getAuthHeader();
26 | } else {
27 | window.location.href = '/login';
28 | }
29 | return headers;
30 | };
31 |
32 | export const post = async ({
33 | url,
34 | body,
35 | headers,
36 | }: {
37 | url: string
38 | body: object
39 | headers?: object
40 | }): Promise => {
41 | const response = await fetch(buildURL(url), {
42 | method: 'POST',
43 | cache: 'no-cache',
44 | headers: {
45 | ...getDefaultHeaders(),
46 | ...headers,
47 | },
48 | body: JSON.stringify(body),
49 | });
50 | const data = await response.json();
51 | if (response.ok) return data;
52 | return Promise.reject(new RequestError(response));
53 | };
54 |
55 | export const put = async ({
56 | url,
57 | body,
58 | headers,
59 | }: {
60 | url: string
61 | body: object
62 | headers?: object
63 | }): Promise => {
64 | const response = await fetch(buildURL(url), {
65 | method: 'PUT',
66 | cache: 'no-cache',
67 | headers: {
68 | ...getDefaultHeaders(),
69 | ...headers,
70 | },
71 | body: JSON.stringify(body),
72 | });
73 | const data = await response.json();
74 | if (response.ok) return data;
75 | return Promise.reject(new RequestError(response));
76 | };
77 |
78 | export const patch = async ({
79 | url,
80 | body,
81 | headers,
82 | }: {
83 | url: string
84 | body: object
85 | headers?: object
86 | }): Promise => {
87 | const response = await fetch(buildURL(url), {
88 | method: 'PATCH',
89 | cache: 'no-cache',
90 | headers: {
91 | ...getDefaultHeaders(),
92 | ...headers,
93 | },
94 | body: JSON.stringify(body),
95 | });
96 | const data = await response.json();
97 | if (response.ok) return data;
98 | return Promise.reject(new RequestError(response));
99 | };
100 |
101 | export const get = async ({
102 | url,
103 | headers,
104 | }: {
105 | url: string
106 | headers?: object
107 | }): Promise => {
108 | const response = await fetch(buildURL(url), {
109 | method: 'GET',
110 | cache: 'no-cache',
111 | headers: {
112 | ...getDefaultHeaders(),
113 | ...headers,
114 | },
115 | });
116 | const data = await response.json();
117 | if (response.ok) return data;
118 | return Promise.reject(new RequestError(response));
119 | };
120 |
121 | export const unauthenticatedRequest = async ({
122 | url,
123 | method,
124 | body,
125 | }: {
126 | url: string,
127 | method: string,
128 | headers?: object
129 | body?: object
130 | }): Promise => {
131 | const response = await fetch(buildURL(url), {
132 | method,
133 | cache: 'no-cache',
134 | headers: { 'Content-Type': 'application/json' },
135 | body: body ? JSON.stringify(body) : undefined,
136 | });
137 | const data = await response.json();
138 | if (response.ok) return data;
139 | return Promise.reject(new RequestError(response));
140 | };
141 |
--------------------------------------------------------------------------------
/src/app/modular/auth/actions.ts:
--------------------------------------------------------------------------------
1 | import { action, Action } from 'typesafe-actions';
2 | import { ThunkAction } from 'redux-thunk';
3 | import { RootState } from '../../types';
4 | import * as request from '../../../client';
5 | import { saveJWT, clearJWT } from '../../../client/jwt';
6 | import { User } from './types';
7 | import { saveCurrentUser, clearCurrentUser } from '../../../client/user';
8 |
9 | export const LOGIN_START = 'auth/loginStart';
10 | export const LOGIN_SUCCESS = 'auth/loginSuccess';
11 | export const LOGIN_ERROR = 'auth/loginError';
12 | export const LOGOUT_SUCCESS = 'auth/logoutSuccess';
13 | export const CLEAR_ERROR = 'auth/clearError';
14 | export const SIGNUP_START = 'auth/signupStart';
15 | export const SIGNUP_SUCCESS = 'auth/signupSuccess';
16 | export const SIGNUP_ERROR = 'auth/signupError';
17 |
18 | export const loginStart = () => action(LOGIN_START);
19 |
20 | export const loginError = (error: string) => action(LOGIN_ERROR, { error });
21 |
22 | export const loginSuccess = (user: User) => action(LOGIN_SUCCESS, { user });
23 |
24 | export const logoutSuccess = () => action(LOGOUT_SUCCESS);
25 |
26 | export const clearError = () => action(CLEAR_ERROR);
27 |
28 | export const signupStart = () => action(SIGNUP_START);
29 |
30 | export const signupSuccess = (user: User) => action(SIGNUP_SUCCESS, { user });
31 |
32 | export const signupError = (error: string) => action(SIGNUP_ERROR, { error });
33 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
34 | export const login = (
35 | {
36 | email,
37 | password,
38 | onLoginSuccess,
39 | onLoginFailure,
40 | }: {
41 | email: string,
42 | password: string,
43 | onLoginSuccess: VoidFunction,
44 | onLoginFailure: VoidFunction
45 | },
46 | ): ThunkAction => (async (dispatch) => {
47 | dispatch(loginStart());
48 | try {
49 | const { token, user }: any = await request.unauthenticatedRequest({
50 | method: 'POST',
51 | url: 'login',
52 | body: { email, password },
53 | });
54 | saveJWT(token);
55 | user.updatedAt = new Date(user.updatedAt);
56 | saveCurrentUser(user);
57 | dispatch(loginSuccess(user));
58 | onLoginSuccess();
59 | } catch (err) {
60 | // eslint-disable-next-line no-console
61 | console.error(err);
62 | dispatch(loginError(err.message as string));
63 | onLoginFailure();
64 | }
65 | });
66 |
67 | export const logout = (
68 | ): ThunkAction => (async (dispatch) => {
69 | try {
70 | clearCurrentUser();
71 | clearJWT();
72 | dispatch(logoutSuccess());
73 | } catch (err) {
74 | // eslint-disable-next-line no-console
75 | console.error(err);
76 | }
77 | });
78 |
79 | export const signup = (
80 | {
81 | firstName,
82 | lastName,
83 | email,
84 | password,
85 | onSignupSuccess,
86 | onSignupFailure,
87 | }:
88 | {
89 | firstName: string,
90 | lastName: string,
91 | email: string,
92 | password: string,
93 | onSignupSuccess: VoidFunction,
94 | onSignupFailure: VoidFunction
95 | },
96 | ): ThunkAction => (async (dispatch) => {
97 | dispatch(signupStart());
98 | try {
99 | const { token, user }: any = await request.unauthenticatedRequest({
100 | method: 'POST',
101 | url: 'users',
102 | body: {
103 | firstName, lastName, email, password,
104 | },
105 | });
106 | saveJWT(token);
107 | user.updatedAt = new Date(user.updatedAt);
108 | saveCurrentUser(user);
109 | dispatch(signupSuccess(user));
110 | onSignupSuccess();
111 | } catch (err) {
112 | // eslint-disable-next-line no-console
113 | console.error(err);
114 | dispatch(signupError(err.message as string));
115 | onSignupFailure();
116 | }
117 | });
118 |
--------------------------------------------------------------------------------
/src/components/Header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { compose } from 'redux';
3 | import { connect } from 'react-redux';
4 | import {
5 | makeStyles, withStyles, createStyles, Theme,
6 | } from '@material-ui/core/styles';
7 | import {
8 | AppBar, Toolbar, Typography, Button, Switch, Grid,
9 | } from '@material-ui/core';
10 | import Moon from './img/moon.png';
11 | import Sun from './img/sun.png';
12 | import { useThemeMode } from '../../theme';
13 | import LogoutButton from '../LogoutButton';
14 | import { RootState } from '../../app/types';
15 | import authDuck from '../../app/modular/auth';
16 |
17 | const DarkModeSwitch = withStyles((theme: Theme) => createStyles({
18 | root: {
19 | width: 80,
20 | height: 48,
21 | padding: 8,
22 | },
23 | switchBase: {
24 | padding: 11,
25 | color: '#ff6a00',
26 | },
27 | thumb: {
28 | width: 26,
29 | height: 26,
30 | backgroundColor: '#fff',
31 | },
32 | track: {
33 | background: `${theme.palette.grey[800]} !important`,
34 | backgroundSize: '26px 25px !important',
35 | backgroundImage: `url(${Sun}) !important`,
36 | backgroundPosition: '80% 48% !important',
37 | backgroundRepeat: 'no-repeat !important',
38 | opacity: '1 !important',
39 | borderRadius: 20,
40 | position: 'relative',
41 | },
42 | checked: {
43 | '&$switchBase': {
44 | color: '#185a9d',
45 | transform: 'translateX(32px)',
46 | '&:hover': {
47 | backgroundColor: 'rgba(24,90,257,0.08)',
48 | },
49 | },
50 | '& $thumb': {
51 | backgroundColor: '#fff',
52 | },
53 | '& + $track': {
54 | background: `${theme.palette.grey[800]} !important`,
55 | backgroundSize: '15px 25px !important',
56 | backgroundImage: `url(${Moon}) !important`,
57 | backgroundPosition: '25% 50% !important',
58 | backgroundRepeat: 'no-repeat !important',
59 | },
60 | },
61 | }))(Switch);
62 |
63 | const useStyles = makeStyles(() => ({
64 | root: {
65 | flexGrow: 1,
66 | },
67 | title: {
68 | flexGrow: 1,
69 | },
70 | }));
71 |
72 | interface ConnectedProps {
73 | loggedIn: boolean;
74 | }
75 |
76 | type Props = ConnectedProps;
77 |
78 | const Header: React.FC = ({ loggedIn }: Props) => {
79 | const classes = useStyles();
80 | const currentTheme = useThemeMode();
81 |
82 | const toggleTheme = useCallback(() => {
83 | localStorage.setItem('themeType',
84 | localStorage.getItem('themeType') === 'dark' ? 'light' : 'dark');
85 | }, []);
86 |
87 | return (
88 |
89 |
90 |
91 |
92 | Talent
93 |
94 |
95 |
96 |
97 |
98 |
99 | {loggedIn ?
100 | : (
101 |
104 | )}
105 |
106 |
107 |
114 |
115 |
116 |
117 |
118 |
119 | );
120 | };
121 |
122 | const mapStateToProps = (state: RootState): Props => ({
123 | loggedIn: authDuck.selectors.loggedIn(state),
124 | });
125 |
126 | export default compose(
127 | connect(mapStateToProps, null),
128 | )(Header);
129 |
--------------------------------------------------------------------------------
/src/pages/Login/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Field, FormikProps, Form, withFormik,
4 | } from 'formik';
5 | import { compose } from 'redux';
6 | import { connect } from 'react-redux';
7 | import { TextField } from 'formik-material-ui';
8 | import {
9 | Container,
10 | CssBaseline,
11 | makeStyles,
12 | Avatar,
13 | Typography,
14 | Button,
15 | Paper,
16 | Grid,
17 | Link,
18 | } from '@material-ui/core';
19 | import { LockOutlined } from '@material-ui/icons';
20 | import * as yup from 'yup';
21 | import { withRouter } from 'react-router';
22 | import { RouteComponentProps } from 'react-router-dom';
23 |
24 | import authDuck from '../../app/modular/auth';
25 |
26 | const useStyles = makeStyles((theme) => ({
27 | paper: {
28 | marginTop: theme.spacing(9),
29 | display: 'flex',
30 | flexDirection: 'column',
31 | alignItems: 'center',
32 | },
33 | avatar: {
34 | margin: theme.spacing(1),
35 | backgroundColor: theme.palette.secondary.main,
36 | },
37 | form: {
38 | width: '100%',
39 | marginTop: theme.spacing(1),
40 | alignItems: 'center',
41 | },
42 | submit: {
43 | margin: theme.spacing(3, 0, 2),
44 | },
45 | input: {
46 | width: '100%',
47 | marginTop: theme.spacing(1),
48 | },
49 | formContainer: {
50 | padding: theme.spacing(2),
51 | marginTop: theme.spacing(4),
52 | },
53 | }));
54 |
55 | interface ConnectedActions {
56 | login: typeof authDuck.actions.login;
57 | }
58 |
59 | interface FormValues {
60 | email: string;
61 | password: string;
62 | }
63 |
64 | type Props = ConnectedActions & FormikProps & RouteComponentProps;
65 |
66 | const LoginPage: React.FC = ({ handleSubmit }: Props) => {
67 | const classes = useStyles();
68 | return (
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Sign in
77 |
78 |
79 |
109 |
110 |
111 |
112 | Forgot password?
113 |
114 |
115 |
116 |
117 | Don't have an account? Sign Up
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | );
126 | };
127 |
128 | const mapPropsToDispatch = {
129 | login: authDuck.actions.login,
130 | };
131 |
132 | const enhanceForm = withFormik({
133 | validateOnMount: true,
134 | mapPropsToValues: () => ({
135 | email: '',
136 | password: '',
137 | }),
138 | validationSchema: yup.object().shape({
139 | email: yup.string().min(1).required('Enter your email.'),
140 | password: yup.string().min(1).required('Enter your password.'),
141 | }),
142 | handleSubmit: (
143 | { email, password }: FormValues,
144 | { props: { login }, setSubmitting, setFieldValue }:
145 | { props: Props, setSubmitting: Function, setFieldValue: Function },
146 | ) => {
147 | login({
148 | email,
149 | password,
150 | onLoginSuccess() { setSubmitting(false); window.location.href = '/home'; },
151 | onLoginFailure() { setSubmitting(false); setFieldValue('password', '', false); },
152 | });
153 | },
154 | displayName: 'login',
155 | });
156 |
157 | export default compose(
158 | connect(null, mapPropsToDispatch),
159 | enhanceForm,
160 | withRouter,
161 | )(LoginPage);
162 |
--------------------------------------------------------------------------------
/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | /* eslint-disable no-console */
3 | // This optional code is used to register a service worker.
4 | // register() is not called by default.
5 |
6 | // This lets the app load faster on subsequent visits in production, and gives
7 | // it offline capabilities. However, it also means that developers (and users)
8 | // will only see deployed updates on subsequent visits to a page, after all the
9 | // existing tabs open on the page have been closed, since previously cached
10 | // resources are updated in the background.
11 |
12 | // To learn more about the benefits of this model and instructions on how to
13 | // opt-in, read https://bit.ly/CRA-PWA
14 |
15 | const isLocalhost = Boolean(
16 | window.location.hostname === 'localhost'
17 | // [::1] is the IPv6 localhost address.
18 | || window.location.hostname === '[::1]'
19 | // 127.0.0.0/8 are considered localhost for IPv4.
20 | || window.location.hostname.match(
21 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
22 | ),
23 | );
24 |
25 | type Config = {
26 | onSuccess?: (registration: ServiceWorkerRegistration) => void;
27 | onUpdate?: (registration: ServiceWorkerRegistration) => void;
28 | };
29 |
30 | function registerValidSW(swUrl: string, config?: Config) {
31 | navigator.serviceWorker
32 | .register(swUrl)
33 | .then((registration) => {
34 | registration.onupdatefound = () => {
35 | const installingWorker = registration.installing;
36 | if (installingWorker == null) {
37 | return;
38 | }
39 | installingWorker.onstatechange = () => {
40 | if (installingWorker.state === 'installed') {
41 | if (navigator.serviceWorker.controller) {
42 | // At this point, the updated precached content has been fetched,
43 | // but the previous service worker will still serve the older
44 | // content until all client tabs are closed.
45 | console.log(
46 | 'New content is available and will be used when all '
47 | + 'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
48 | );
49 |
50 | // Execute callback
51 | if (config && config.onUpdate) {
52 | config.onUpdate(registration);
53 | }
54 | } else {
55 | // At this point, everything has been precached.
56 | // It's the perfect time to display a
57 | // "Content is cached for offline use." message.
58 | console.log('Content is cached for offline use.');
59 |
60 | // Execute callback
61 | if (config && config.onSuccess) {
62 | config.onSuccess(registration);
63 | }
64 | }
65 | }
66 | };
67 | };
68 | })
69 | .catch((error) => {
70 | console.error('Error during service worker registration:', error);
71 | });
72 | }
73 |
74 | function checkValidServiceWorker(swUrl: string, config?: Config) {
75 | // Check if the service worker can be found. If it can't reload the page.
76 | fetch(swUrl, {
77 | headers: { 'Service-Worker': 'script' },
78 | })
79 | .then((response) => {
80 | // Ensure service worker exists, and that we really are getting a JS file.
81 | const contentType = response.headers.get('content-type');
82 | if (
83 | response.status === 404
84 | || (contentType != null && contentType.indexOf('javascript') === -1)
85 | ) {
86 | // No service worker found. Probably a different app. Reload the page.
87 | navigator.serviceWorker.ready.then((registration) => {
88 | registration.unregister().then(() => {
89 | window.location.reload();
90 | });
91 | });
92 | } else {
93 | // Service worker found. Proceed as normal.
94 | registerValidSW(swUrl, config);
95 | }
96 | })
97 | .catch(() => {
98 | console.log(
99 | 'No internet connection found. App is running in offline mode.',
100 | );
101 | });
102 | }
103 |
104 | export function register(config?: Config) {
105 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
106 | // The URL constructor is available in all browsers that support SW.
107 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
108 | if (publicUrl.origin !== window.location.origin) {
109 | // Our service worker won't work if PUBLIC_URL is on a different origin
110 | // from what our page is served on. This might happen if a CDN is used to
111 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
112 | return;
113 | }
114 |
115 | window.addEventListener('load', () => {
116 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
117 |
118 | if (isLocalhost) {
119 | // This is running on localhost. Let's check if a service worker still exists or not.
120 | checkValidServiceWorker(swUrl, config);
121 |
122 | // Add some additional logging to localhost, pointing developers to the
123 | // service worker/PWA documentation.
124 | navigator.serviceWorker.ready.then(() => {
125 | console.log(
126 | 'This web app is being served cache-first by a service '
127 | + 'worker. To learn more, visit https://bit.ly/CRA-PWA',
128 | );
129 | });
130 | } else {
131 | // Is not localhost. Just register service worker
132 | registerValidSW(swUrl, config);
133 | }
134 | });
135 | }
136 | }
137 |
138 | export function unregister() {
139 | if ('serviceWorker' in navigator) {
140 | navigator.serviceWorker.ready
141 | .then((registration) => {
142 | registration.unregister();
143 | })
144 | .catch((error) => {
145 | console.error(error.message);
146 | });
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/pages/Signup/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Form, FormikProps, Field, withFormik,
4 | } from 'formik';
5 | import { compose } from 'redux';
6 | import { connect } from 'react-redux';
7 | import { TextField } from 'formik-material-ui';
8 | import {
9 | Avatar,
10 | Button,
11 | CssBaseline,
12 | Link,
13 | Grid,
14 | Typography,
15 | Container,
16 | Paper,
17 | } from '@material-ui/core';
18 | import { makeStyles } from '@material-ui/core/styles';
19 | import BorderColorIcon from '@material-ui/icons/BorderColor';
20 | import * as Yup from 'yup';
21 | import { withRouter } from 'react-router';
22 | import { RouteComponentProps } from 'react-router-dom';
23 |
24 | import authDuck from '../../app/modular/auth';
25 |
26 | const useStyles = makeStyles((theme) => ({
27 | paper: {
28 | marginTop: theme.spacing(9),
29 | display: 'flex',
30 | flexDirection: 'column',
31 | alignItems: 'center',
32 | },
33 | avatar: {
34 | margin: theme.spacing(1),
35 | backgroundColor: theme.palette.secondary.main,
36 | },
37 | form: {
38 | width: '100%',
39 | marginTop: theme.spacing(1),
40 | alignItems: 'center',
41 | },
42 | submit: {
43 | margin: theme.spacing(3, 0, 2),
44 | },
45 | input: {
46 | width: '100%',
47 | marginTop: theme.spacing(1),
48 | },
49 | formContainer: {
50 | padding: theme.spacing(2),
51 | marginTop: theme.spacing(4),
52 | },
53 | }));
54 |
55 | interface ConnectedActions {
56 | signup: typeof authDuck.actions.signup;
57 | }
58 |
59 | interface FormValues {
60 | firstName: string;
61 | lastName: string;
62 | email: string;
63 | password: string;
64 | reenter_password: string;
65 | }
66 |
67 | type Props = ConnectedActions & FormikProps & RouteComponentProps;
68 | const Signup: React.FC = ({ handleSubmit }: Props) => {
69 | const classes = useStyles();
70 |
71 | return (
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | Sign up
80 |
81 |
82 |
145 |
146 |
147 |
148 | Already have an account? Sign in
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | );
157 | };
158 |
159 | const mapPropsToDispatch = {
160 | signup: authDuck.actions.signup,
161 | };
162 |
163 | const enhanceForm = withFormik({
164 | validateOnMount: true,
165 | mapPropsToValues: () => ({
166 | firstName: '',
167 | lastName: '',
168 | email: '',
169 | password: '',
170 | reenter_password: '',
171 | }),
172 | validationSchema: Yup.object().shape({
173 | firstName: Yup.string().min(1).required('First Name is required'),
174 | lastName: Yup.string().min(1).required('Last Name is required'),
175 | email: Yup.string().min(1).required('Enter your email').email('Enter a valid email'),
176 | password: Yup.string().min(8, 'Password must contain at least 8 characters').required('Enter your password'),
177 | reenter_password: Yup.string().required('Confirm your password').oneOf([Yup.ref('password')], 'Password does not match'),
178 | }),
179 | handleSubmit: (
180 | {
181 | firstName,
182 | lastName,
183 | email,
184 | password,
185 | }: FormValues,
186 | { props: { signup }, setSubmitting, setFieldValue }:
187 | { props: Props, setSubmitting: Function, setFieldValue: Function },
188 | ) => {
189 | signup({
190 | firstName,
191 | lastName,
192 | email,
193 | password,
194 | onSignupSuccess() { setSubmitting(false); window.location.href = '/home'; },
195 | onSignupFailure() { setSubmitting(false); setFieldValue('email', '', false); setFieldValue('password', '', false); setFieldValue('reenter_password', '', false); },
196 | });
197 | },
198 | displayName: 'signup',
199 | });
200 |
201 | export default compose(
202 | connect(null, mapPropsToDispatch),
203 | enhanceForm,
204 | withRouter,
205 | )(Signup);
206 |
--------------------------------------------------------------------------------