├── .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 | 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 |
{ 32 | e.preventDefault(); 33 | submitPost({ 34 | variables: { description, url, userId: user.sub }, 35 | refetchQueries: [{query: POSTS_LIST}] 36 | }) 37 | .catch(function(error) { 38 | console.log(error); 39 | setError(error.toString()); 40 | }); 41 | //You are having a controlled component where input value is determined by this.state.city. 42 | // So once you submit you have to clear your state which will clear your input automatically. 43 | setDescription(''); 44 | setUrl(''); 45 | } 46 | } 47 | > 48 | 49 | 50 | Post Title: 51 | 52 | 53 | 54 | 55 | setDescription( e.target.value )} 58 | type="text" 59 | /> 60 | 61 | 62 | 63 | 64 | Url (Enter a valid url, doesn't check for validity currently): 65 | 66 | 67 | 68 | 69 | setUrl( e.target.value )} 72 | type="text" 73 | /> 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |
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 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | } --------------------------------------------------------------------------------