├── .env ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── App.scss ├── AuthenticatedApp.js ├── UnauthenticatedApp.js ├── context └── Authentication.js ├── hooks ├── useAsync.js └── useToken.js ├── index.js ├── routes ├── Private.js └── Public.js ├── screens ├── Home │ └── Home.js └── Login │ └── Login.js └── utils ├── api-client.js └── auth-provider.js /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL=http://localhost:4000/api -------------------------------------------------------------------------------- /.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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inspirationjon/react-app-starter/75b7a7f98905c269b64ceada49f77f381fa0bd7b/README.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starter-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.9", 7 | "@testing-library/react": "^11.2.5", 8 | "@testing-library/user-event": "^12.8.3", 9 | "node-sass": "^5.0.0", 10 | "react": "^17.0.1", 11 | "react-dom": "^17.0.1", 12 | "react-query": "^3.13.12", 13 | "react-router-dom": "^5.2.0", 14 | "react-scripts": "4.0.3", 15 | "web-vitals": "^1.1.1" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inspirationjon/react-app-starter/75b7a7f98905c269b64ceada49f77f381fa0bd7b/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inspirationjon/react-app-starter/75b7a7f98905c269b64ceada49f77f381fa0bd7b/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inspirationjon/react-app-starter/75b7a7f98905c269b64ceada49f77f381fa0bd7b/public/logo512.png -------------------------------------------------------------------------------- /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 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import './App.scss' 2 | import AuthenticatedApp from './AuthenticatedApp.js' 3 | import UnauthenticatedApp from './UnauthenticatedApp.js' 4 | import useToken from './hooks/useToken' 5 | 6 | function App() { 7 | const [token] = useToken() 8 | if (token) { 9 | return 10 | } else { 11 | return 12 | } 13 | } 14 | 15 | export default App 16 | -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inspirationjon/react-app-starter/75b7a7f98905c269b64ceada49f77f381fa0bd7b/src/App.scss -------------------------------------------------------------------------------- /src/AuthenticatedApp.js: -------------------------------------------------------------------------------- 1 | import './App.scss' 2 | import { Switch, Route } from 'react-router-dom' 3 | import Home from './screens/Home/Home' 4 | 5 | function AuthenticatedApp() { 6 | return ( 7 | <> 8 |
9 | 10 | 11 | 12 |
13 | 14 | ) 15 | } 16 | 17 | export default AuthenticatedApp 18 | -------------------------------------------------------------------------------- /src/UnauthenticatedApp.js: -------------------------------------------------------------------------------- 1 | import './App.scss' 2 | import Login from './screens/Login/Login' 3 | 4 | function UnauthenticatedApp() { 5 | return ( 6 | <> 7 |
8 | 9 |
10 | 11 | ) 12 | } 13 | 14 | export default UnauthenticatedApp 15 | -------------------------------------------------------------------------------- /src/context/Authentication.js: -------------------------------------------------------------------------------- 1 | import { createContext, useEffect, useState } from 'react' 2 | 3 | const Context = createContext(null) 4 | 5 | function Provider({ children }) { 6 | const [state, setState] = useState(window.localStorage.getItem('token')) 7 | 8 | useEffect(() => { 9 | if (state) { 10 | window.localStorage.setItem('token', state) 11 | } else { 12 | window.localStorage.removeItem('token') 13 | } 14 | }, [state]) 15 | 16 | return ( 17 | 18 | {children} 19 | 20 | ) 21 | } 22 | 23 | export { Context, Provider } 24 | -------------------------------------------------------------------------------- /src/hooks/useAsync.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | function useSafeDispatch(dispatch) { 4 | const mounted = React.useRef(false) 5 | React.useLayoutEffect(() => { 6 | mounted.current = true 7 | return () => (mounted.current = false) 8 | }, []) 9 | return React.useCallback( 10 | (...args) => (mounted.current ? dispatch(...args) : void 0), 11 | [dispatch] 12 | ) 13 | } 14 | 15 | // Example usage: 16 | // const {data, error, status, run} = useAsync() 17 | // React.useEffect(() => { 18 | // run(fetchPokemon(pokemonName)) 19 | // }, [pokemonName, run]) 20 | const defaultInitialState = { status: 'idle', data: null, error: null } 21 | function useAsync(initialState) { 22 | const initialStateRef = React.useRef({ 23 | ...defaultInitialState, 24 | ...initialState, 25 | }) 26 | const [{ status, data, error }, setState] = React.useReducer( 27 | (s, a) => ({ ...s, ...a }), 28 | initialStateRef.current 29 | ) 30 | 31 | const safeSetState = useSafeDispatch(setState) 32 | 33 | const setData = React.useCallback( 34 | (data) => safeSetState({ data, status: 'resolved' }), 35 | [safeSetState] 36 | ) 37 | const setError = React.useCallback( 38 | (error) => safeSetState({ error, status: 'rejected' }), 39 | [safeSetState] 40 | ) 41 | const reset = React.useCallback( 42 | () => safeSetState(initialStateRef.current), 43 | [safeSetState] 44 | ) 45 | 46 | const run = React.useCallback( 47 | (promise) => { 48 | if (!promise || !promise.then) { 49 | throw new Error( 50 | `The argument passed to useAsync().run must be a promise. Maybe a function that's passed isn't returning anything?` 51 | ) 52 | } 53 | safeSetState({ status: 'pending' }) 54 | return promise.then( 55 | (data) => { 56 | setData(data) 57 | return data 58 | }, 59 | (error) => { 60 | setError(error) 61 | return Promise.reject(error) 62 | } 63 | ) 64 | }, 65 | [safeSetState, setData, setError] 66 | ) 67 | 68 | return { 69 | // using the same names that react-query uses for convenience 70 | isIdle: status === 'idle', 71 | isLoading: status === 'pending', 72 | isError: status === 'rejected', 73 | isSuccess: status === 'resolved', 74 | 75 | setData, 76 | setError, 77 | error, 78 | status, 79 | data, 80 | run, 81 | reset, 82 | } 83 | } 84 | 85 | export { useAsync } 86 | -------------------------------------------------------------------------------- /src/hooks/useToken.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { Context } from '../context/Authentication' 3 | 4 | const useToken = (setterOnly) => { 5 | const ctx = useContext(Context) 6 | 7 | return setterOnly ? [ctx.setState] : [ctx.state, ctx.setState] 8 | } 9 | 10 | export default useToken 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import { BrowserRouter } from 'react-router-dom' 5 | import { Provider as Authentication } from './context/Authentication' 6 | import { QueryClient, QueryClientProvider } from 'react-query' 7 | const queryClient = new QueryClient() 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | , 19 | document.getElementById('root') 20 | ) 21 | -------------------------------------------------------------------------------- /src/routes/Private.js: -------------------------------------------------------------------------------- 1 | import { Route, Redirect } from 'react-router-dom' 2 | import useToken from '../hooks/useToken' 3 | 4 | function Private({ children, ...props }) { 5 | const [token] = useToken(false) 6 | 7 | if (!token) { 8 | return 9 | } 10 | 11 | return 12 | } 13 | 14 | export default Private 15 | -------------------------------------------------------------------------------- /src/routes/Public.js: -------------------------------------------------------------------------------- 1 | import { useLocation, Redirect, Route } from 'react-router-dom' 2 | import useToken from '../hooks/useToken' 3 | 4 | function Public({ children, ...props }) { 5 | const { pathname } = useLocation() 6 | 7 | const [token] = useToken(false) 8 | 9 | if (token && pathname === '/login') { 10 | return 11 | } 12 | 13 | return 14 | } 15 | 16 | export default Public 17 | -------------------------------------------------------------------------------- /src/screens/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Home() { 4 | return
Home
5 | } 6 | 7 | export default Home 8 | -------------------------------------------------------------------------------- /src/screens/Login/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Login() { 4 | return
Login
5 | } 6 | 7 | export default Login 8 | -------------------------------------------------------------------------------- /src/utils/api-client.js: -------------------------------------------------------------------------------- 1 | import * as auth from './auth-provider' 2 | const apiURL = process.env.REACT_APP_API_URL 3 | 4 | function client( 5 | endpoint, 6 | { data, token, headers: customHeaders, ...customConfig } = {} 7 | ) { 8 | const config = { 9 | method: data ? 'POST' : 'GET', 10 | body: data ? JSON.stringify(data) : undefined, 11 | headers: { 12 | Authorization: token ? `Bearer ${token}` : undefined, 13 | 'Content-Type': data ? 'application/json' : undefined, 14 | ...customHeaders, 15 | }, 16 | ...customConfig, 17 | } 18 | 19 | return window 20 | .fetch(`${apiURL}/${endpoint}`, config) 21 | .then(async (response) => { 22 | if (response.status === 401) { 23 | await auth.logout() 24 | // refresh the page for them 25 | window.location.assign(window.location) 26 | return Promise.reject({ message: 'Please re-authenticate.' }) 27 | } 28 | const data = await response.json() 29 | if (response.ok) { 30 | return data 31 | } else { 32 | return Promise.reject(data) 33 | } 34 | }) 35 | } 36 | 37 | export { client } 38 | -------------------------------------------------------------------------------- /src/utils/auth-provider.js: -------------------------------------------------------------------------------- 1 | import { client } from './api-client' 2 | 3 | const localStorageKey = '__auth_provider_token__' 4 | 5 | async function getToken() { 6 | return window.localStorage.getItem(localStorageKey) 7 | } 8 | 9 | function handleUserResponse({ user }) { 10 | window.localStorage.setItem(localStorageKey, user.token) 11 | return user 12 | } 13 | 14 | function login({ username, password }) { 15 | return client('login', { username, password }).then(handleUserResponse) 16 | } 17 | 18 | function register({ username, password }) { 19 | return client('register', { username, password }).then(handleUserResponse) 20 | } 21 | 22 | async function logout() { 23 | window.localStorage.removeItem(localStorageKey) 24 | } 25 | 26 | export { getToken, login, register, logout, localStorageKey } 27 | --------------------------------------------------------------------------------