├── .github
└── FUNDING.yml
├── .gitignore
├── .travis.yml
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── components
├── Account
│ └── index.js
├── App
│ ├── index.css
│ └── index.js
├── Comments
│ └── index.js
├── Form
│ └── index.js
├── FrontPage
│ └── index.js
├── Navigation
│ └── index.js
├── PasswordChange
│ └── index.js
├── PasswordForget
│ └── index.js
├── Readings
│ └── index.js
├── Session
│ ├── AuthUserContext.js
│ ├── withAuthentication.js
│ └── withAuthorization.js
├── SignIn
│ └── index.js
├── SignOut
│ └── index.js
├── SignUp
│ └── index.js
├── StoryItem
│ └── index.js
└── StoryList
│ └── index.js
├── constants
└── routes.js
├── firebase
├── auth.js
├── db.js
├── firebase.js
└── index.js
├── index.css
├── index.js
└── registerServiceWorker.js
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: rwieruch
4 | patreon: # rwieruch
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with a single custom sponsorship URL
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - stable
5 |
6 | install:
7 | - npm install
8 |
9 | script:
10 | - npm test
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The Road beyond React (Application)
2 |
3 | [](https://travis-ci.org/rwieruch/road-beyond-react-app) [](https://slack-the-road-to-learn-react.wieruch.com/)
4 |
5 | A Hacker News / Pocket clone which let's you save front page stories for later. Found in [the Road beyond React](https://www.roadtolearnreact.com/).
6 |
7 |
8 |
9 | ## Features
10 |
11 | * uses:
12 | * only React (create-react-app)
13 | * firebase
14 | * react-router 4
15 | * semantic-ui
16 | * styled-components
17 | * no Redux/MobX
18 | * features:
19 | * Sign In
20 | * Sign Up
21 | * Sign Out
22 | * Password Forget
23 | * Password Change
24 | * Protected Routes with Authorization
25 | * Database: Users, Stories
26 |
27 | ## Installation
28 |
29 | * `git clone git@github.com:rwieruch/road-beyond-react-app.git`
30 | * `cd road-beyond-react-app`
31 | * `npm install`
32 | * `npm start`
33 | * visit http://localhost:3000/
34 | * use your own Firebase Credentials
35 |
36 | ### Use your own Firebase Credentials
37 |
38 | * visit https://firebase.google.com/ and create a Firebase App
39 | * copy and paste your Credentials from your Firebase App into src/firebase/firebase.js
40 | * activate Email/Password Sign-In Method in your Firebase App
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-firebase-authentication",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "firebase": "^4.12.1",
8 | "react": "^16.3.0",
9 | "react-dom": "^16.3.0",
10 | "react-router-dom": "^4.2.2",
11 | "react-scripts": "1.1.1",
12 | "semantic-ui-css": "^2.3.1",
13 | "semantic-ui-react": "^0.79.0",
14 | "styled-components": "^3.2.5"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test --env=jsdom",
20 | "eject": "react-scripts eject"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rwieruch/road-beyond-react-app/706042626b5c0ae5c1db607eeb7d0278369a79d3/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/Account/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { PasswordForgetForm } from '../PasswordForget';
4 | import PasswordChangeForm from '../PasswordChange';
5 | import withAuthorization from '../Session/withAuthorization';
6 |
7 | const AccountPage = () =>
8 |
12 |
13 | export default withAuthorization(true)(AccountPage);
--------------------------------------------------------------------------------
/src/components/App/index.css:
--------------------------------------------------------------------------------
1 | .app {
2 | margin: 20px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/App/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { BrowserRouter as Router, Route } from 'react-router-dom';
3 | import axios from 'axios';
4 |
5 | import Navigation from '../Navigation';
6 | import FrontPage from '../FrontPage';
7 | import ReadingsPage from '../Readings';
8 | import SignUpPage from '../SignUp';
9 | import SignInPage from '../SignIn';
10 | import PasswordForgetPage from '../PasswordForget';
11 | import AccountPage from '../Account';
12 | import withAuthentication from '../Session/withAuthentication';
13 | import { firebase, db } from '../../firebase';
14 | import * as routes from '../../constants/routes';
15 |
16 | import './index.css';
17 |
18 | const HN_URL = 'http://hn.algolia.com/api/v1/search';
19 |
20 | class App extends Component {
21 | constructor(props) {
22 | super(props);
23 |
24 | this.state = {
25 | stories: null,
26 | storiesLoading: false,
27 | storiesError: null,
28 |
29 | readingsInit: null,
30 | readings: null,
31 | readingsLoading: null,
32 | };
33 |
34 | this.onFetchStories = this.onFetchStories.bind(this);
35 | this.onFetchReadings = this.onFetchReadings.bind(this);
36 | }
37 |
38 | componentDidMount() {
39 | this.onFetchStories();
40 |
41 | firebase.auth.onAuthStateChanged(authUser => {
42 | if (!this.state.readingsInit && authUser) {
43 | this.onFetchReadings(authUser);
44 |
45 | this.setState({ readingsInit: true });
46 | }
47 | });
48 | }
49 |
50 | onFetchStories() {
51 | this.setState({ storiesLoading: true })
52 |
53 | axios(`${HN_URL}?tags=front_page`)
54 | .then(result => this.setState((prevState) => ({
55 | stories: result.data.hits,
56 | storiesLoading: false,
57 | })))
58 | .catch(error => this.setState({
59 | storiesError: error,
60 | storiesLoading: false,
61 | }));
62 | }
63 |
64 | onFetchReadings(authUser) {
65 | this.setState(() => ({ readingsLoading: true }));
66 |
67 | db.onGetReadings(authUser, (snapshot) =>
68 | this.setState(() => ({
69 | readings: snapshot.val(),
70 | readingsLoading: false,
71 | }))
72 | );
73 | }
74 |
75 | render() {
76 | return (
77 |
78 |
79 |
80 |
81 |
82 |
83 |
86 |
89 | }
90 | />
91 |
92 |
95 |
98 | }
99 | />
100 |
101 |
105 |
109 |
113 |
117 |
118 |
119 | );
120 | }
121 | }
122 |
123 | export default withAuthentication(App);
--------------------------------------------------------------------------------
/src/components/Comments/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { List, Loader } from 'semantic-ui-react';
3 | import axios from 'axios';
4 | import styled from 'styled-components';
5 |
6 | const CommentsBlock = styled.div`
7 | padding-left: 10px;
8 | border-left: 1px solid #e0e1e2;
9 | `;
10 |
11 | const Comment = styled.div`
12 | padding: 10px 0;
13 | `;
14 |
15 | class Comments extends Component {
16 | constructor(props) {
17 | super(props);
18 |
19 | this.state = {
20 | story: null,
21 | storyLoading: false,
22 | storyError: false,
23 | }
24 | }
25 |
26 | componentDidMount() {
27 | const { objectID } = this.props.story;
28 |
29 | this.setState({ storyLoading: true });
30 |
31 | axios(`http://hn.algolia.com/api/v1/items/${objectID}`)
32 | .then(result => this.setState({
33 | story: result.data,
34 | storyLoading: false
35 | }))
36 | .catch(error => this.setState({
37 | storyError: error,
38 | storyLoading: false,
39 | }));
40 | }
41 |
42 | render() {
43 | const {
44 | story,
45 | storyLoading,
46 | storyError,
47 | } = this.state;
48 |
49 | if (storyLoading) {
50 | return ;
51 | }
52 |
53 | if (storyError) {
54 | return Uuup, something went wrong.
;
55 | }
56 |
57 | return story && ;
58 | }
59 | }
60 |
61 | const CommentList = ({ comments }) =>
62 |
63 |
64 | {comments.map(comment =>
65 |
66 |
67 |
68 | {comment.author}
69 | :{' '}
70 |
73 |
74 |
75 | {comment.children.length
76 | ?
77 | : null
78 | }
79 |
80 | )}
81 |
82 |
83 |
84 | export default Comments;
--------------------------------------------------------------------------------
/src/components/Form/index.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Form = styled.form`
4 | display: flex;
5 | flex-direction: column;
6 | border: 1px solid #e0e1e2;
7 | box-shadow: 5px 5px 10px #e0e1e2;
8 | border-radius: 5px;
9 | margin: 20px;
10 | padding: 20px;
11 |
12 | .ui {
13 | margin-bottom: 10px;
14 | }
15 | `;
16 |
17 | // TODO box shadow
18 |
19 | export default Form;
--------------------------------------------------------------------------------
/src/components/FrontPage/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Loader } from 'semantic-ui-react';
3 |
4 | import StoryList from '../StoryList';
5 |
6 | const FrontPage = ({
7 | readings,
8 | stories,
9 | storiesLoading,
10 | storiesError,
11 | }) => {
12 | if (storiesError) {
13 | return Uuups, something went wrong.
;
14 | }
15 |
16 | if (storiesLoading) {
17 | return ;
18 | }
19 |
20 | if (!stories) {
21 | return Uuups, there are no more front page stories for you.
;
22 | }
23 |
24 | const readableStories = readings
25 | ? stories.filter(story => !readings[story.objectID])
26 | : stories;
27 |
28 | if (!readableStories.length) {
29 | return Uuups, there are no more front page stories for you.
;
30 | }
31 |
32 | return
36 | }
37 |
38 | export default FrontPage;
39 |
--------------------------------------------------------------------------------
/src/components/Navigation/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Menu } from 'semantic-ui-react';
4 |
5 | import AuthUserContext from '../Session/AuthUserContext';
6 | import SignOutButton from '../SignOut';
7 | import * as routes from '../../constants/routes';
8 |
9 | const Navigation = (props) =>
10 |
11 | {authUser => authUser
12 | ?
13 | :
14 | }
15 |
16 |
17 | const NavigationAuth = () =>
18 |
33 |
34 | const NavigationNonAuth = () =>
35 |
44 |
45 | export default Navigation;
--------------------------------------------------------------------------------
/src/components/PasswordChange/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Input, Button } from 'semantic-ui-react';
3 |
4 | import Form from '../Form';
5 | import { auth } from '../../firebase';
6 |
7 | const INITIAL_STATE = {
8 | passwordOne: '',
9 | passwordTwo: '',
10 | error: null,
11 | };
12 |
13 | class PasswordChangeForm extends Component {
14 | constructor(props) {
15 | super(props);
16 |
17 | this.state = { ...INITIAL_STATE };
18 | }
19 |
20 | onSubmit = (event) => {
21 | const { passwordOne } = this.state;
22 |
23 | auth.doPasswordUpdate(passwordOne)
24 | .then(() => {
25 | this.setState(() => ({ ...INITIAL_STATE }));
26 | })
27 | .catch(error => {
28 | this.setState(() => ({ error }));
29 | });
30 |
31 | event.preventDefault();
32 | }
33 |
34 | render() {
35 | const {
36 | passwordOne,
37 | passwordTwo,
38 | error,
39 | } = this.state;
40 |
41 | const isInvalid =
42 | passwordOne !== passwordTwo ||
43 | passwordOne === '';
44 |
45 | return (
46 |
65 | );
66 | }
67 | }
68 |
69 | export default PasswordChangeForm;
--------------------------------------------------------------------------------
/src/components/PasswordForget/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Input, Button } from 'semantic-ui-react';
4 |
5 | import Form from '../Form';
6 | import { auth } from '../../firebase';
7 |
8 | const INITIAL_STATE = {
9 | email: '',
10 | error: null,
11 | };
12 |
13 | class PasswordForgetForm extends Component {
14 | constructor(props) {
15 | super(props);
16 |
17 | this.state = { ...INITIAL_STATE };
18 | }
19 |
20 | onSubmit = (event) => {
21 | const { email } = this.state;
22 |
23 | auth.doPasswordReset(email)
24 | .then(() => {
25 | this.setState(() => ({ ...INITIAL_STATE }));
26 | })
27 | .catch(error => {
28 | this.setState(() => ({ error }));
29 | });
30 |
31 | event.preventDefault();
32 | }
33 |
34 | render() {
35 | const {
36 | email,
37 | error,
38 | } = this.state;
39 |
40 | const isInvalid = email === '';
41 |
42 | return (
43 |
56 | );
57 | }
58 | }
59 |
60 | const PasswordForgetLink = () =>
61 |
62 | Forgot Password?
63 |
64 |
65 | export default PasswordForgetForm;
66 |
67 | export {
68 | PasswordForgetForm,
69 | PasswordForgetLink,
70 | };
71 |
--------------------------------------------------------------------------------
/src/components/Readings/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch, Route } from 'react-router-dom';
3 | import { Loader, List } from 'semantic-ui-react';
4 |
5 | import withAuthorization from '../Session/withAuthorization';
6 | import StoryItem from '../StoryItem';
7 | import Comments from '../Comments';
8 | import * as routes from '../../constants/routes';
9 |
10 | const ReadingListPage = ({
11 | readings,
12 | readingsLoading,
13 | }) => {
14 | if (readingsLoading) {
15 | return ;
16 | }
17 |
18 | if (!readings) {
19 | return Uuups, you don't have any saved readings yet.
;
20 | }
21 |
22 | return (
23 |
24 |
27 |
30 | }
31 | />
32 |
33 |
36 |
40 | }
41 | />
42 |
43 | );
44 | }
45 |
46 | const ReadingItem = ({
47 | readings,
48 | match,
49 | }) => {
50 | const story = readings[match.params.id];
51 |
52 | return (
53 |
54 |
58 |
59 |
60 | );
61 | }
62 |
63 | const ReadingList = ({ readings }) =>
64 |
65 | {Object.keys(readings).map(key =>
66 |
71 | )}
72 |
73 |
74 | export default withAuthorization(true)(ReadingListPage);
--------------------------------------------------------------------------------
/src/components/Session/AuthUserContext.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const AuthUserContext = React.createContext(null);
4 |
5 | export default AuthUserContext;
--------------------------------------------------------------------------------
/src/components/Session/withAuthentication.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import AuthUserContext from './AuthUserContext';
4 | import { firebase } from '../../firebase';
5 |
6 | const withAuthentication = (Component) =>
7 | class WithAuthentication extends React.Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.state = {
12 | authUser: null,
13 | };
14 | }
15 |
16 | componentDidMount() {
17 | firebase.auth.onAuthStateChanged(authUser => {
18 | authUser
19 | ? this.setState(() => ({ authUser }))
20 | : this.setState(() => ({ authUser: null }));
21 | });
22 | }
23 |
24 | render() {
25 | const { authUser } = this.state;
26 | return (
27 |
28 |
29 |
30 | );
31 | }
32 | }
33 |
34 | export default withAuthentication;
--------------------------------------------------------------------------------
/src/components/Session/withAuthorization.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withRouter } from 'react-router-dom';
3 |
4 | import AuthUserContext from './AuthUserContext';
5 | import { firebase } from '../../firebase';
6 | import * as routes from '../../constants/routes';
7 |
8 | const withAuthorization = (needsAuthorization) => (Component) => {
9 | class WithAuthorization extends React.Component {
10 | componentDidMount() {
11 | firebase.auth.onAuthStateChanged(authUser => {
12 | if (!authUser && needsAuthorization) {
13 | this.props.history.push(routes.SIGN_IN);
14 | }
15 | });
16 | }
17 |
18 | render() {
19 | return (
20 |
21 | {authUser => authUser
22 | ?
23 | : null
24 | }
25 |
26 | );
27 | }
28 | }
29 |
30 | return withRouter(WithAuthorization);
31 | }
32 |
33 | export default withAuthorization;
--------------------------------------------------------------------------------
/src/components/SignIn/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { withRouter } from 'react-router-dom';
3 | import { Input, Button } from 'semantic-ui-react';
4 | import styled from 'styled-components';
5 |
6 | import Form from '../Form';
7 | import { SignUpLink } from '../SignUp';
8 | import { PasswordForgetLink } from '../PasswordForget';
9 | import { auth } from '../../firebase';
10 | import * as routes from '../../constants/routes';
11 |
12 | const SignInAdditional = styled.div`
13 | display: flex;
14 | flex-direction: column;
15 | align-items: center;
16 | `;
17 |
18 | const SignInPage = ({ history }) =>
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | const INITIAL_STATE = {
28 | email: '',
29 | password: '',
30 | error: null,
31 | };
32 |
33 | class SignInForm extends Component {
34 | constructor(props) {
35 | super(props);
36 |
37 | this.state = { ...INITIAL_STATE };
38 | }
39 |
40 | onSubmit = (event) => {
41 | const {
42 | email,
43 | password,
44 | } = this.state;
45 |
46 | const {
47 | history,
48 | } = this.props;
49 |
50 | auth.doSignInWithEmailAndPassword(email, password)
51 | .then(() => {
52 | this.setState(() => ({ ...INITIAL_STATE }));
53 | history.push(routes.READING_LIST);
54 | })
55 | .catch(error => {
56 | this.setState(() => ({ error }));
57 | });
58 |
59 | event.preventDefault();
60 | }
61 |
62 | render() {
63 | const {
64 | email,
65 | password,
66 | error,
67 | } = this.state;
68 |
69 | const isInvalid =
70 | password === '' ||
71 | email === '';
72 |
73 | return (
74 |
93 | );
94 | }
95 | }
96 |
97 | export default withRouter(SignInPage);
98 |
99 | export {
100 | SignInForm,
101 | };
102 |
--------------------------------------------------------------------------------
/src/components/SignOut/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from 'semantic-ui-react';
3 |
4 | import { auth } from '../../firebase';
5 |
6 | const SignOutButton = () =>
7 |
13 |
14 | export default SignOutButton;
15 |
--------------------------------------------------------------------------------
/src/components/SignUp/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import {
3 | Link,
4 | withRouter,
5 | } from 'react-router-dom';
6 | import { Input, Button } from 'semantic-ui-react';
7 |
8 | import Form from '../Form';
9 | import { auth, db } from '../../firebase';
10 | import * as routes from '../../constants/routes';
11 |
12 | const INITIAL_STATE = {
13 | username: '',
14 | email: '',
15 | passwordOne: '',
16 | passwordTwo: '',
17 | error: null,
18 | };
19 |
20 | class SignUpForm extends Component {
21 | constructor(props) {
22 | super(props);
23 |
24 | this.state = { ...INITIAL_STATE };
25 | }
26 |
27 | onSubmit = (event) => {
28 | const {
29 | username,
30 | email,
31 | passwordOne,
32 | } = this.state;
33 |
34 | const {
35 | history,
36 | } = this.props;
37 |
38 | auth.doCreateUserWithEmailAndPassword(email, passwordOne)
39 | .then(authUser => {
40 |
41 | // Create a user in your own accessible Firebase Database too
42 | db.doCreateUser(authUser.uid, username, email)
43 | .then(() => {
44 | this.setState(() => ({ ...INITIAL_STATE }));
45 | history.push(routes.READING_LIST);
46 | })
47 | .catch(error => {
48 | this.setState(() => ({ error }));
49 | });
50 |
51 | })
52 | .catch(error => {
53 | this.setState(() => ({ error }));
54 | });
55 |
56 | event.preventDefault();
57 | }
58 |
59 | render() {
60 | const {
61 | username,
62 | email,
63 | passwordOne,
64 | passwordTwo,
65 | error,
66 | } = this.state;
67 |
68 | const isInvalid =
69 | passwordOne !== passwordTwo ||
70 | passwordOne === '' ||
71 | username === '';
72 |
73 | return (
74 |
105 | );
106 | }
107 | }
108 |
109 | const SignUpLink = () =>
110 |
111 | Don't have an account?
112 | {' '}
113 | Sign Up
114 |
115 |
116 | export default withRouter(SignUpForm);
117 |
118 | export {
119 | SignUpForm,
120 | SignUpLink,
121 | };
--------------------------------------------------------------------------------
/src/components/StoryItem/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Button, List, Icon, Label } from 'semantic-ui-react';
4 | import styled from 'styled-components';
5 |
6 | import AuthUserContext from '../Session/AuthUserContext';
7 | import * as routes from '../../constants/routes';
8 | import { db } from '../../firebase';
9 |
10 | const StoryRow = styled.div`
11 | margin: 10px 0;
12 | display: flex;
13 | align-items: baseline;
14 | `;
15 |
16 | const StoryContentItem = styled.div`
17 | margin-right: 10px;
18 | `;
19 |
20 | const StoryItem = ({
21 | story,
22 | isFrontPage,
23 | isReadingsPage,
24 | }) =>
25 |
26 |
27 |
28 | {authUser =>
29 |
30 |
31 | {isFrontPage && }
35 |
36 |
37 | {story.title}
38 |
39 |
40 |
41 | {isReadingsPage &&
42 |
43 |
51 |
52 |
53 |
54 |
60 |
61 |
62 |
66 | }
67 |
68 | }
69 |
70 |
71 |
72 |
73 | class ReadLaterButton extends Component {
74 | constructor(props) {
75 | super(props);
76 |
77 | this.state = {
78 | readLaterSuccess: null,
79 | readLaterError: null,
80 | };
81 |
82 | this.onReadLater = this.onReadLater.bind(this);
83 | }
84 |
85 | onReadLater(story, authUser) {
86 | db.doCreateReading(authUser, story)
87 | .then(() => {
88 | this.setState(() => ({ readLaterSuccess: true }));
89 | })
90 | .catch(() => {
91 | this.setState(() => ({ readLaterError: true }));
92 | });
93 | }
94 |
95 | render() {
96 | const { story, authUser } = this.props;
97 | const { readLaterSuccess, readLaterError } = this.state;
98 |
99 | if (!authUser) {
100 | return null;
101 | }
102 |
103 | if (readLaterSuccess) {
104 | return Saved;
105 | }
106 |
107 | if (readLaterError) {
108 | return Uuups;
109 | }
110 |
111 | return (
112 |
120 | );
121 | }
122 | }
123 |
124 | const DismissButton = ({
125 | story,
126 | authUser,
127 | }) => {
128 | if (!authUser) {
129 | return null;
130 | }
131 |
132 | return (
133 |
140 | );
141 | }
142 |
143 | export default StoryItem;
--------------------------------------------------------------------------------
/src/components/StoryList/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { List } from 'semantic-ui-react';
3 |
4 | import StoryItem from '../StoryItem';
5 |
6 | const StoryList = ({
7 | stories,
8 | isFrontPage,
9 | isReadingsPage,
10 | }) =>
11 |
12 | {stories.map(story =>
13 |
19 |
20 |
21 | )}
22 |
23 |
24 | const StoryHeader = ({ story }) =>
25 |
26 | {story.title}
27 | {' '}
28 | by
29 | {' '}
30 | {story.author}
31 |
32 |
33 | export default StoryList;
--------------------------------------------------------------------------------
/src/constants/routes.js:
--------------------------------------------------------------------------------
1 | export const FRONTPAGE = '/';
2 | export const READING_LIST = '/reading-list';
3 | export const SIGN_UP = '/signup';
4 | export const SIGN_IN = '/signin';
5 | export const PASSWORD_FORGET = '/pw-forget';
6 | export const ACCOUNT = '/account';
7 |
--------------------------------------------------------------------------------
/src/firebase/auth.js:
--------------------------------------------------------------------------------
1 | import { auth } from './firebase';
2 |
3 | // Sign Up
4 | export const doCreateUserWithEmailAndPassword = (email, password) =>
5 | auth.createUserWithEmailAndPassword(email, password);
6 |
7 | // Sign In
8 | export const doSignInWithEmailAndPassword = (email, password) =>
9 | auth.signInWithEmailAndPassword(email, password);
10 |
11 | // Sign out
12 | export const doSignOut = () =>
13 | auth.signOut();
14 |
15 | // Password Reset
16 | export const doPasswordReset = (email) =>
17 | auth.sendPasswordResetEmail(email);
18 |
19 | // Password Change
20 | export const doPasswordUpdate = (password) =>
21 | auth.currentUser.updatePassword(password);
22 |
--------------------------------------------------------------------------------
/src/firebase/db.js:
--------------------------------------------------------------------------------
1 | import { db } from './firebase';
2 |
3 | // User API
4 |
5 | export const doCreateUser = (id, username, email) =>
6 | db.ref(`users/${id}`)
7 | .set({
8 | username,
9 | email,
10 | });
11 |
12 | // Readings API
13 |
14 | export const doCreateReading = (authUser, story) =>
15 | db.ref(`users/${authUser.uid}/readings/${story.objectID}`)
16 | .set(story);
17 |
18 | export const doRemoveReading = (authUser, story) =>
19 | db.ref(`users/${authUser.uid}/readings/${story.objectID}`)
20 | .remove();
21 |
22 | export const onGetReadings = (authUser, fn) =>
23 | db.ref(`users/${authUser.uid}/readings`)
24 | .on('value', fn);
25 |
--------------------------------------------------------------------------------
/src/firebase/firebase.js:
--------------------------------------------------------------------------------
1 | import * as firebase from 'firebase';
2 |
3 | const prodConfig = {
4 | apiKey: 'AIzaSyBqGNzKlC88TuT281WcWPYFWmz8Ib8UQ4s',
5 | authDomain: 'road-beyond-react-app.firebaseapp.com',
6 | databaseURL: 'https://road-beyond-react-app.firebaseio.com',
7 | projectId: 'road-beyond-react-app',
8 | storageBucket: 'road-beyond-react-app.appspot.com',
9 | messagingSenderId: '779454115476'
10 | };
11 |
12 | const devConfig = {
13 | apiKey: 'AIzaSyBqGNzKlC88TuT281WcWPYFWmz8Ib8UQ4s',
14 | authDomain: 'road-beyond-react-app.firebaseapp.com',
15 | databaseURL: 'https://road-beyond-react-app.firebaseio.com',
16 | projectId: 'road-beyond-react-app',
17 | storageBucket: 'road-beyond-react-app.appspot.com',
18 | messagingSenderId: '779454115476'
19 | };
20 |
21 | const config = process.env.NODE_ENV === 'production'
22 | ? prodConfig
23 | : devConfig;
24 |
25 | if (!firebase.apps.length) {
26 | firebase.initializeApp(config);
27 | }
28 |
29 | const db = firebase.database();
30 | const auth = firebase.auth();
31 |
32 | export {
33 | db,
34 | auth,
35 | };
36 |
--------------------------------------------------------------------------------
/src/firebase/index.js:
--------------------------------------------------------------------------------
1 | import * as auth from './auth';
2 | import * as db from './db';
3 | import * as firebase from './firebase';
4 |
5 | export {
6 | auth,
7 | db,
8 | firebase,
9 | };
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: sans-serif;
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './components/App';
4 | import registerServiceWorker from './registerServiceWorker';
5 |
6 | import './index.css';
7 | import 'semantic-ui-css/semantic.min.css';
8 |
9 | ReactDOM.render(, document.getElementById('root'));
10 | registerServiceWorker();
11 |
--------------------------------------------------------------------------------
/src/registerServiceWorker.js:
--------------------------------------------------------------------------------
1 | // In production, we register a service worker to serve assets from local cache.
2 |
3 | // This lets the app load faster on subsequent visits in production, and gives
4 | // it offline capabilities. However, it also means that developers (and users)
5 | // will only see deployed updates on the "N+1" visit to a page, since previously
6 | // cached resources are updated in the background.
7 |
8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
9 | // This link also includes instructions on opting out of this behavior.
10 |
11 | const isLocalhost = Boolean(
12 | window.location.hostname === 'localhost' ||
13 | // [::1] is the IPv6 localhost address.
14 | window.location.hostname === '[::1]' ||
15 | // 127.0.0.1/8 is considered localhost for IPv4.
16 | window.location.hostname.match(
17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
18 | )
19 | );
20 |
21 | export default function register() {
22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
23 | // The URL constructor is available in all browsers that support SW.
24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
25 | if (publicUrl.origin !== window.location.origin) {
26 | // Our service worker won't work if PUBLIC_URL is on a different origin
27 | // from what our page is served on. This might happen if a CDN is used to
28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
29 | return;
30 | }
31 |
32 | window.addEventListener('load', () => {
33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
34 |
35 | if (!isLocalhost) {
36 | // Is not local host. Just register service worker
37 | registerValidSW(swUrl);
38 | } else {
39 | // This is running on localhost. Lets check if a service worker still exists or not.
40 | checkValidServiceWorker(swUrl);
41 | }
42 | });
43 | }
44 | }
45 |
46 | function registerValidSW(swUrl) {
47 | navigator.serviceWorker
48 | .register(swUrl)
49 | .then(registration => {
50 | registration.onupdatefound = () => {
51 | const installingWorker = registration.installing;
52 | installingWorker.onstatechange = () => {
53 | if (installingWorker.state === 'installed') {
54 | if (navigator.serviceWorker.controller) {
55 | // At this point, the old content will have been purged and
56 | // the fresh content will have been added to the cache.
57 | // It's the perfect time to display a "New content is
58 | // available; please refresh." message in your web app.
59 | console.log('New content is available; please refresh.');
60 | } else {
61 | // At this point, everything has been precached.
62 | // It's the perfect time to display a
63 | // "Content is cached for offline use." message.
64 | console.log('Content is cached for offline use.');
65 | }
66 | }
67 | };
68 | };
69 | })
70 | .catch(error => {
71 | console.error('Error during service worker registration:', error);
72 | });
73 | }
74 |
75 | function checkValidServiceWorker(swUrl) {
76 | // Check if the service worker can be found. If it can't reload the page.
77 | fetch(swUrl)
78 | .then(response => {
79 | // Ensure service worker exists, and that we really are getting a JS file.
80 | if (
81 | response.status === 404 ||
82 | response.headers.get('content-type').indexOf('javascript') === -1
83 | ) {
84 | // No service worker found. Probably a different app. Reload the page.
85 | navigator.serviceWorker.ready.then(registration => {
86 | registration.unregister().then(() => {
87 | window.location.reload();
88 | });
89 | });
90 | } else {
91 | // Service worker found. Proceed as normal.
92 | registerValidSW(swUrl);
93 | }
94 | })
95 | .catch(() => {
96 | console.log(
97 | 'No internet connection found. App is running in offline mode.'
98 | );
99 | });
100 | }
101 |
102 | export function unregister() {
103 | if ('serviceWorker' in navigator) {
104 | navigator.serviceWorker.ready.then(registration => {
105 | registration.unregister();
106 | });
107 | }
108 | }
109 |
--------------------------------------------------------------------------------