├── .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 |
--------------------------------------------------------------------------------