├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
└── src
├── App.test.js
├── auth
├── auth_config.json
└── react-auth0-wrapper.js
├── components
├── App.js
├── NewPost.js
├── Post.js
├── PostList.js
├── Profile.js
├── SecuredRoute.js
└── header.js
├── index.js
├── logo.svg
├── serviceWorker.js
└── styles
├── App.css
└── index.css
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HackerNews Clone using React, Apollo-React-Client and Hasura GraphQl Engine
2 |
3 | Note: This project was build as a part of a tutorial blog post, check out the tutorial [here](https://hasura.io/blog/hackernews-tutorial-using-graphql-react-hasura-part1/).
4 |
5 | [Live Demo](https://hackernews-1919.herokuapp.com/)
6 | This application demonstrates consuming GraphQl Api provided by [Hasura GraphQL Engine](https://hasura.io) using a react app. Uses react-apollo GraphQL client to make requests to the api. Users can create account using [Auth0 JWT authentication](https://auth0.com/) which is then verified by Hasura. React-router is used to provide SPA experience.
7 |
8 | Authenticated users can:
9 | * Create new posts
10 | * Upvote posts
11 | * Realtime updates when other users upvote a post or create a new one (updating apollo cache).
12 |
13 | ## Installation
14 |
15 | Installing and running on local system require:
16 | * [Setting up Hasura Server](https://docs.hasura.io/1.0/graphql/manual/getting-started/heroku-simple.html) (deployed on Heroku), and creating required tables
17 | * [Setting up Auth0](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0)
18 | * See [this](https://docs.hasura.io/1.0/graphql/manual/guides/integrations/auth0-jwt.html) guide for Auth0 JWT Integration with Hasura
19 | * Clone or download this repo, install the required packages and run `npm start`
20 |
21 | ## npm packages:
22 |
23 | You will need the following npm packages:
24 | * [react-router-dom](https://www.npmjs.com/package/react-router-dom)
25 | * [react-bootstrap](https://www.npmjs.com/package/react-bootstrap)
26 | * [apollo-boost](https://www.npmjs.com/package/apollo-boost)
27 | * [@apollo/react-hooks](https://www.npmjs.com/package/@apollo/react-hooks)
28 | * [apollo-link-context](https://www.npmjs.com/package/apollo-link-context)
29 | * [@apollo/react-hoc](https://www.npmjs.com/package/@apollo/react-hoc)
30 | * [graphql](https://www.npmjs.com/package/graphql)
31 | * [@auth0/auth0-spa-js](https://www.npmjs.com/package/@auth0/auth0-spa-js)
32 |
33 |
34 | ## Creating tables
35 |
36 | Following tables required to be created:
37 | ```
38 | type Post {
39 | id - integer, primary key
40 | description - text
41 | url - text
42 | created_at - timestamp with time zone
43 | user_id - text
44 | }
45 |
46 | type Users {
47 | name - text
48 | last_seen - timestamp with time zone
49 | id - text, primary key
50 | }
51 |
52 | type Point {
53 | id - integer, primary key
54 | user_id - text
55 | post_id - integer
56 | }
57 | ```
58 | `Post.user_id` and `Users.id` have object relationship. `Point.post_id` and `Post.id` have array relationship. Permissions should be given accordingly.
59 |
60 | ## User Authentication
61 |
62 | See [Setting up Auth0 with react](https://auth0.com/docs/quickstart/spa/react/01-login#configure-auth0) and [this](https://docs.hasura.io/1.0/graphql/manual/guides/integrations/auth0-jwt.html) guide for Auth0 JWT Integration with Hasura. Here we are using Auth0 Universal Login.
63 |
64 | ## Realtime updates
65 |
66 | Using apollo cache and react state, we can give realtime updates for upvotes and new posts. Apollo `refetchQueries` function updates apollo cache with refetched data.
67 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hackernews-clone",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@apollo/react-hoc": "^3.0.1",
7 | "@apollo/react-hooks": "^3.0.1",
8 | "@auth0/auth0-spa-js": "^1.1.1",
9 | "apollo-boost": "^0.4.4",
10 | "apollo-link-context": "^1.0.18",
11 | "auth0-js": "^9.11.3",
12 | "bootstrap": "^4.3.1",
13 | "graphql": "^14.4.2",
14 | "graphql-tag": "^2.10.1",
15 | "react": "^16.8.6",
16 | "react-apollo": "^2.5.8",
17 | "react-bootstrap": "^1.0.0-beta.10",
18 | "react-dom": "^16.8.6",
19 | "react-router": "^5.0.1",
20 | "react-router-dom": "^5.0.1",
21 | "react-scripts": "3.0.1"
22 | },
23 | "scripts": {
24 | "start": "react-scripts start",
25 | "build": "react-scripts build",
26 | "test": "react-scripts test",
27 | "eject": "react-scripts eject"
28 | },
29 | "eslintConfig": {
30 | "extends": "react-app"
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abhi40308/hackernews-clone/7bfa80efb48ab60ece5adff84ab3a92db9872662/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
9 |
10 |
11 |
15 |
16 |
25 | Hacker News
26 |
27 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
44 |
48 |
49 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './components/App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/auth/auth_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "domain": "dev-z5h3dp-b.auth0.com",
3 | "clientId": "Yqqr7YtTcH5Fulpmqzl8uF4xnOop7UZK",
4 | "redirect_uri": "https://hackernews-1919.herokuapp.com",
5 | "audience": "hackernews-clone"
6 | }
7 |
--------------------------------------------------------------------------------
/src/auth/react-auth0-wrapper.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useContext } from "react";
2 | import createAuth0Client from "@auth0/auth0-spa-js";
3 |
4 | const DEFAULT_REDIRECT_CALLBACK = () =>
5 | window.history.replaceState({}, document.title, window.location.pathname);
6 |
7 | export const Auth0Context = React.createContext();
8 | export const useAuth0 = () => useContext(Auth0Context);
9 | export const Auth0Provider = ({
10 | children,
11 | onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
12 | ...initOptions
13 | }) => {
14 | const [isAuthenticated, setIsAuthenticated] = useState();
15 | const [user, setUser] = useState();
16 | const [auth0Client, setAuth0] = useState();
17 | const [loading, setLoading] = useState(true);
18 | const [popupOpen, setPopupOpen] = useState(false);
19 |
20 | useEffect(() => {
21 | const initAuth0 = async () => {
22 | const auth0FromHook = await createAuth0Client(initOptions);
23 | setAuth0(auth0FromHook);
24 |
25 | if (window.location.search.includes("code=")) {
26 | const { appState } = await auth0FromHook.handleRedirectCallback();
27 | onRedirectCallback(appState);
28 | }
29 |
30 | const isAuthenticated = await auth0FromHook.isAuthenticated();
31 |
32 | setIsAuthenticated(isAuthenticated);
33 |
34 | if (isAuthenticated) {
35 | const user = await auth0FromHook.getUser();
36 | setUser(user);
37 | }
38 |
39 | setLoading(false);
40 | };
41 | initAuth0();
42 | // eslint-disable-next-line
43 | }, []);
44 |
45 | const loginWithPopup = async (params = {}) => {
46 | setPopupOpen(true);
47 | try {
48 | await auth0Client.loginWithPopup(params);
49 | } catch (error) {
50 | console.error(error);
51 | } finally {
52 | setPopupOpen(false);
53 | }
54 | const user = await auth0Client.getUser();
55 | setUser(user);
56 | setIsAuthenticated(true);
57 | };
58 |
59 | const handleRedirectCallback = async () => {
60 | setLoading(true);
61 | await auth0Client.handleRedirectCallback();
62 | const user = await auth0Client.getUser();
63 | setLoading(false);
64 | setIsAuthenticated(true);
65 | setUser(user);
66 | };
67 | return (
68 | auth0Client.getIdTokenClaims(...p),
77 | loginWithRedirect: (...p) => auth0Client.loginWithRedirect(...p),
78 | getTokenSilently: (...p) => auth0Client.getTokenSilently(...p),
79 | getTokenWithPopup: (...p) => auth0Client.getTokenWithPopup(...p),
80 | logout: (...p) => auth0Client.logout(...p)
81 | }}
82 | >
83 | {children}
84 |
85 | );
86 | };
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "../styles/App.css";
3 | import Header from "./header.js";
4 | import PostList from "./PostList";
5 | import NewPost from "./NewPost";
6 | import Profile from "./Profile"
7 |
8 | // for authentication using auth0
9 | import { useAuth0 } from "../auth/react-auth0-wrapper";
10 |
11 | // for routing
12 | import { Switch, Route } from "react-router-dom";
13 | import SecuredRoute from "./SecuredRoute";
14 |
15 | // for apollo client
16 | import { ApolloProvider } from "@apollo/react-hooks";
17 | import { ApolloClient, HttpLink, InMemoryCache } from "apollo-boost";
18 | import { setContext } from "apollo-link-context";
19 |
20 | function App() {
21 |
22 | // used state to get accessToken through getTokenSilently(), the component re-renders when state changes, thus we have
23 | // our accessToken in apollo client instance.
24 | const [accessToken, setAccessToken] = useState("");
25 |
26 | const { getTokenSilently, loading } = useAuth0();
27 | if (loading) {
28 | return "Loading...";
29 | }
30 |
31 | // get access token
32 | const getAccessToken = async () => {
33 | // getTokenSilently() returns a promise
34 | try {
35 | const token = await getTokenSilently();
36 | setAccessToken(token);
37 | } catch (e) {
38 | console.log(e);
39 | }
40 | };
41 | getAccessToken();
42 |
43 | // for apollo client
44 | const httpLink = new HttpLink({
45 | uri: "https://hackernews-clone-2.herokuapp.com/v1/graphql"
46 | });
47 |
48 | const authLink = setContext((_, { headers }) => {
49 | const token = accessToken;
50 | if (token) {
51 | return {
52 | headers: {
53 | ...headers,
54 | authorization: `Bearer ${token}`
55 | }
56 | };
57 | } else {
58 | return {
59 | headers: {
60 | ...headers
61 | }
62 | };
63 | }
64 | });
65 |
66 | const client = new ApolloClient({
67 | link: authLink.concat(httpLink),
68 | cache: new InMemoryCache()
69 | });
70 |
71 | return (
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
83 | export default App;
84 |
--------------------------------------------------------------------------------
/src/components/NewPost.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "../styles/index.css";
3 | import { Container, Row, Col } from "react-bootstrap";
4 | import { useMutation } from "@apollo/react-hooks";
5 | import { gql } from "apollo-boost";
6 | import { useAuth0 } from "../auth/react-auth0-wrapper";
7 | import { withApollo } from "@apollo/react-hoc";
8 | import {POSTS_LIST} from "./PostList";
9 |
10 | const SUBMIT_POST = gql`
11 | mutation ($description: String!, $url: String!, $userId: String!) {
12 | insert_post(objects: [{description: $description, url: $url, user_id:$userId}])
13 | {
14 | affected_rows
15 | }
16 | }
17 | `;
18 |
19 | function NewPost() {
20 | const { user } = useAuth0();
21 | const [description, setDescription] = useState("");
22 | const [url, setUrl] = useState("");
23 | const [error, setError] = useState("");
24 |
25 | const [submitPost] = useMutation(SUBMIT_POST);
26 |
27 | return (
28 | <>
29 |
30 |
82 |
83 | {error}
84 |
85 | >
86 | );
87 | }
88 |
89 | export default withApollo(NewPost);
90 |
--------------------------------------------------------------------------------
/src/components/Post.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Row, Col } from "react-bootstrap";
3 | import { Link } from "react-router-dom";
4 | import "../styles/index.css";
5 | import { gql } from "apollo-boost";
6 | import { useMutation } from "@apollo/react-hooks";
7 | import { POSTS_LIST } from "./PostList";
8 | import { useAuth0 } from "../auth/react-auth0-wrapper";
9 |
10 | const UPVOTE_POST = gql`
11 | mutation($postId: Int!, $userId: String!) {
12 | insert_point(objects: [{ post_id: $postId, user_id: $userId }]) {
13 | affected_rows
14 | }
15 | }
16 | `;
17 |
18 | function Post(props) {
19 | const { isAuthenticated, user } = useAuth0();
20 |
21 | let loggedUserId = "";
22 | if (isAuthenticated) {
23 | loggedUserId = user.sub;
24 | }
25 |
26 | const postdate = new Date(props.post.created_at);
27 |
28 | const [upvotePost] = useMutation(UPVOTE_POST, {
29 | variables: { postId: props.post.id, userId: loggedUserId },
30 | refetchQueries: [{ query: POSTS_LIST }]
31 | });
32 |
33 | return (
34 |
35 | {/* key is just a react thing, you can read it here : https://reactjs.org/docs/lists-and-keys.html#keys */}
36 |
37 |
38 |
39 | {isAuthenticated && (
40 |
41 | ▲
42 |
43 | )}
44 |
45 |
46 | {props.post.description}
47 |
48 |
49 |
50 |
51 |
52 | {props.post.points_aggregate.aggregate.count} points | by
53 |
54 |
55 | {props.post.user.name}
56 |
57 |
58 | created at {postdate.toString()};
59 |
60 |
61 |
62 |
63 | );
64 | }
65 |
66 | export default Post;
67 |
--------------------------------------------------------------------------------
/src/components/PostList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Container } from "react-bootstrap";
3 | import "../styles/index.css";
4 | import { useQuery } from "@apollo/react-hooks";
5 | import { gql } from "apollo-boost";
6 | import { withApollo } from "@apollo/react-hoc";
7 | import Post from "./Post";
8 |
9 | // post sorted in descending order by time of creation
10 | export const POSTS_LIST = gql`
11 | {
12 | post(order_by: { created_at: desc }) {
13 | id
14 | created_at
15 | url
16 | description
17 | user {
18 | id
19 | name
20 | }
21 | points_aggregate {
22 | aggregate {
23 | count(columns: id)
24 | }
25 | }
26 | }
27 | }
28 | `;
29 |
30 | function PostList() {
31 | const { loading, error, data } = useQuery(POSTS_LIST);
32 |
33 | if (loading) return "Loading...";
34 | if (error) return `Error! ${error.message}`;
35 |
36 | return (
37 |
38 |
39 | {data.post.map((post, index) => (
40 |
41 | ))}
42 |
43 |
44 | );
45 | }
46 |
47 | export default withApollo(PostList);
48 |
--------------------------------------------------------------------------------
/src/components/Profile.js:
--------------------------------------------------------------------------------
1 | import { useQuery } from "@apollo/react-hooks";
2 | import React from "react";
3 | import { withRouter } from "react-router";
4 | import { Container, Row, Col } from "react-bootstrap";
5 | import { gql } from "apollo-boost";
6 |
7 | const USER_INFO = gql`
8 | query($id: String!) {
9 | users(where: { id: { _eq: $id } }) {
10 | last_seen
11 | name
12 | }
13 | }
14 | `;
15 |
16 | function Profile(props) {
17 | const { loading, error, data } = useQuery(USER_INFO, {
18 | variables: { id: props.match.params.id }
19 | });
20 | if (loading) return "Loading...";
21 | if (error) return `Error! ${error.message}`;
22 |
23 | return (
24 | <>
25 |
26 | {data.users.map((user, index) => (
27 | <>
28 |
29 | user : {user.name}
30 |
31 |
32 | last seen : {new Date(user.last_seen).toString()}
33 |
34 | >
35 | ))}
36 |
37 | >
38 | );
39 | }
40 |
41 | export default withRouter(Profile);
42 |
--------------------------------------------------------------------------------
/src/components/SecuredRoute.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Route} from 'react-router-dom';
3 | import { useAuth0 } from "../auth/react-auth0-wrapper";
4 |
5 |
6 | function SecuredRoute(props) {
7 | const {component: Component, path} = props;
8 | const { isAuthenticated, loginWithRedirect } = useAuth0();
9 | return (
10 | {
11 | if (!isAuthenticated) {
12 | loginWithRedirect({});
13 | return ;
14 | }
15 | return
16 | }} />
17 | );
18 | }
19 |
20 | export default SecuredRoute;
--------------------------------------------------------------------------------
/src/components/header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withRouter } from "react-router";
3 | import { Navbar, Container, Row } from "react-bootstrap";
4 | import { useAuth0 } from "../auth/react-auth0-wrapper";
5 | import { Link } from "react-router-dom";
6 |
7 | function Header() {
8 | const { isAuthenticated, loginWithRedirect, logout, user } = useAuth0();
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | Hacker News
16 |
17 |
18 |
19 | {!isAuthenticated && (
20 | <>
21 |
27 | >
28 | )}
29 |
30 | {isAuthenticated && (
31 | <>
32 |
33 | submit
34 |
35 | |
36 |
37 | user :
38 |
39 | {user.nickname}
40 |
41 |
42 | |
43 |
46 | >
47 | )}
48 |
49 |
50 |
51 |
52 | );
53 | }
54 |
55 | export default withRouter(Header);
56 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import "./styles/index.css";
4 | import App from "./components/App";
5 | import * as serviceWorker from "./serviceWorker";
6 | import { BrowserRouter } from "react-router-dom";
7 | import { Auth0Provider } from "./auth/react-auth0-wrapper";
8 | import config from "./auth/auth_config.json";
9 |
10 | // A function that routes the user to the right place
11 | // after login
12 | const onRedirectCallback = appState => {
13 | window.history.replaceState(
14 | {},
15 | document.title,
16 | appState && appState.targetUrl
17 | ? appState.targetUrl
18 | : window.location.pathname
19 | );
20 | };
21 |
22 | ReactDOM.render(
23 |
24 |
31 |
32 |
33 | ,
34 | document.getElementById("root")
35 | );
36 | serviceWorker.unregister();
37 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/styles/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | animation: App-logo-spin infinite 20s linear;
7 | height: 40vmin;
8 | pointer-events: none;
9 | }
10 |
11 | .App-header {
12 | background-color: #282c34;
13 | min-height: 100vh;
14 | display: flex;
15 | flex-direction: column;
16 | align-items: center;
17 | justify-content: center;
18 | font-size: calc(10px + 2vmin);
19 | color: white;
20 | }
21 |
22 | .App-link {
23 | color: #61dafb;
24 | }
25 |
26 | @keyframes App-logo-spin {
27 | from {
28 | transform: rotate(0deg);
29 | }
30 | to {
31 | transform: rotate(360deg);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: Verdana, Geneva, sans-serif;
5 | }
6 |
7 | .header {
8 | justify-self: center;
9 | margin-top: 7px;
10 | }
11 |
12 | .navbar {
13 | width: 85%;
14 | padding: 1px 5px;
15 | font-size: 13px;
16 | background-color: #ff6600;
17 | color: black;
18 | }
19 |
20 | .fw {
21 | font-weight: 900;
22 | }
23 |
24 | .font-small {
25 | font-size: 8px;
26 | }
27 |
28 | .anchor {
29 | text-decoration: none;
30 | color: black;
31 | }
32 |
33 | .anchor:hover {
34 | text-decoration: none;
35 | color: black;
36 | }
37 |
38 | .ml-auto {
39 | margin-left: auto;
40 | margin-right: 0px;
41 | }
42 |
43 | .auth-button {
44 | text-decoration: none;
45 | padding: 0;
46 | background: none;
47 | border: none;
48 | }
49 |
50 | .postlist {
51 | width: 85%;
52 | font-size: 13px;
53 | padding: 1px 1px;
54 | background-color: #F6F6EE;
55 | color: black;
56 | }
57 |
58 | .post {
59 | padding: 3px 3px;
60 | }
61 |
62 | .post-id {
63 | color: #848484;
64 | font-size: 12px;
65 | }
66 |
67 | .upvote {
68 | color: #9A9A9A;
69 | }
70 |
71 | .post-url {
72 | text-decoration: none;
73 | color: #848484;
74 | font-size: 10px;
75 | }
76 |
77 | .cursor {
78 | cursor: pointer;
79 | }
--------------------------------------------------------------------------------