├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.js ├── components ├── Billing.js ├── Form.js ├── FormInputs.js ├── OptIn.js ├── ProgressBar.js └── Shipping.js ├── context └── FormContext.js ├── hooks └── useFormContext.js ├── index.css └── index.js /.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 | # "React Form Progress Bar" 2 | 3 | --- 4 | 5 | ### Author Links 6 | 7 | 👋 Hello, I'm Dave Gray. 8 | 9 | ✅ [Check out my YouTube Channel with all of my tutorials](https://www.youtube.com/DaveGrayTeachesCode). 10 | 11 | 🚩 [Subscribe to my channel](https://bit.ly/3nGHmNn) 12 | 13 | ☕ [Buy Me A Coffee](https://buymeacoffee.com/DaveGray) 14 | 15 | 🚀 Follow Me: 16 | 17 | - [Twitter](https://twitter.com/yesdavidgray) 18 | - [LinkedIn](https://www.linkedin.com/in/davidagray/) 19 | - [Blog](https://yesdavidgray.com) 20 | - [Reddit](https://www.reddit.com/user/DaveOnEleven) 21 | 22 | --- 23 | 24 | ### Description 25 | 26 | 📺 [YouTube Video](https://youtu.be/8QOfBYxYy7U) for this repository. 27 | 28 | --- 29 | 30 | ### 💻 Starter Source Code 31 | 32 | - 🔗 [React Multi-Step Form Tutorial - Source Code](https://github.com/gitdagray/react-multi-step-form) 33 | - 34 | ### 💻 Completed Source Code 35 | 36 | - 🔗 [React Form Progress Bar - Completed Source Code](https://github.com/gitdagray/react-form-progress-bar) 37 | 38 | --- 39 | 40 | ### 🎓 Academic Honesty 41 | 42 | **DO NOT COPY FOR AN ASSIGNMENT** - Avoid plagiargism and adhere to the spirit of this [Academic Honesty Policy](https://www.freecodecamp.org/news/academic-honesty-policy/). 43 | 44 | --- 45 | 46 | ### ⚙ VS Code Extensions I Use: 47 | 48 | - 🔗 [ES7 React JS Snippets Extension](https://marketplace.visualstudio.com/items?itemName=dsznajder.es7-react-js-snippets) 49 | - 🔗 [vscode-icons VS Code Extension](https://marketplace.visualstudio.com/items?itemName=vscode-icons-team.vscode-icons) 50 | - 🔗 [Github Themes VS Code Extension](https://marketplace.visualstudio.com/items?itemName=GitHub.github-vscode-theme) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-form-progress-bar", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.5", 7 | "@testing-library/react": "^13.4.0", 8 | "@testing-library/user-event": "^13.5.0", 9 | "react": "^18.2.0", 10 | "react-dom": "^18.2.0", 11 | "react-scripts": "5.0.1", 12 | "web-vitals": "^2.1.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitdagray/react-form-progress-bar/cff5a82b29aa0cd9e95a6b6bc8b08be333ca3f5d/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 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitdagray/react-form-progress-bar/cff5a82b29aa0cd9e95a6b6bc8b08be333ca3f5d/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitdagray/react-form-progress-bar/cff5a82b29aa0cd9e95a6b6bc8b08be333ca3f5d/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 Form from "./components/Form" 2 | import ProgressBar from "./components/ProgressBar" 3 | import { FormProvider } from './context/FormContext' 4 | 5 | function App() { 6 | 7 | return ( 8 | 9 | 10 |
11 | 12 | ) 13 | 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/components/Billing.js: -------------------------------------------------------------------------------- 1 | import useFormContext from "../hooks/useFormContext" 2 | 3 | const Billing = () => { 4 | 5 | const { data, handleChange } = useFormContext() 6 | 7 | const content = ( 8 |
9 |
10 |
11 | 12 | 21 |
22 |
23 | 24 | 33 |
34 |
35 | 36 | 37 | 46 | 47 | 48 | 57 | 58 | 59 | 68 | 69 | 70 | 128 | 129 | 130 | 140 |
141 | ) 142 | 143 | return content 144 | } 145 | export default Billing -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | import FormInputs from './FormInputs' 2 | import useFormContext from "../hooks/useFormContext" 3 | 4 | const Form = () => { 5 | 6 | const { 7 | page, 8 | setPage, 9 | data, 10 | title, 11 | canSubmit, 12 | disablePrev, 13 | disableNext, 14 | prevHide, 15 | nextHide, 16 | submitHide 17 | } = useFormContext() 18 | 19 | const handlePrev = () => setPage(prev => prev - 1) 20 | 21 | const handleNext = () => setPage(prev => prev + 1) 22 | 23 | const handleSubmit = e => { 24 | e.preventDefault() 25 | console.log(JSON.stringify(data)) 26 | } 27 | 28 | 29 | const content = ( 30 | 31 | 32 |
33 |

{title[page]}

34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 | ) 50 | 51 | return content 52 | } 53 | export default Form -------------------------------------------------------------------------------- /src/components/FormInputs.js: -------------------------------------------------------------------------------- 1 | import Billing from "./Billing" 2 | import OptIn from "./OptIn" 3 | import Shipping from "./Shipping" 4 | import useFormContext from "../hooks/useFormContext" 5 | 6 | const FormInputs = () => { 7 | 8 | const { page } = useFormContext() 9 | 10 | const display = { 11 | 0: , 12 | 1: , 13 | 2: 14 | } 15 | 16 | const content = ( 17 |
18 | {display[page]} 19 |
20 | ) 21 | 22 | 23 | return content 24 | } 25 | export default FormInputs -------------------------------------------------------------------------------- /src/components/OptIn.js: -------------------------------------------------------------------------------- 1 | import useFormContext from "../hooks/useFormContext" 2 | 3 | const OptIn = () => { 4 | const { data, handleChange } = useFormContext() 5 | 6 | const content = ( 7 | <> 8 | 12 |
    13 |
  • Save 10% Now
  • 14 |
  • Receive Discount Coupons
  • 15 |
  • Find Out About New Products
  • 16 |
17 | 18 | ) 19 | 20 | return content 21 | } 22 | export default OptIn -------------------------------------------------------------------------------- /src/components/ProgressBar.js: -------------------------------------------------------------------------------- 1 | import useFormContext from '../hooks/useFormContext' 2 | 3 | const ProgressBar = () => { 4 | const { page, title } = useFormContext() 5 | 6 | const interval = 100 / Object.keys(title).length 7 | 8 | const progress = ((page + 1) * interval).toFixed(2) 9 | 10 | const steps = Object.keys(title).map((step, i) => { 11 | return
Step {i + 1}
12 | }) 13 | 14 | // Renders with every input event 15 | // Will fix next => in optimization tutorial 16 | console.log('render') 17 | 18 | return ( 19 |
20 |
21 | {steps} 22 |
23 | 24 |
25 | ) 26 | } 27 | export default ProgressBar -------------------------------------------------------------------------------- /src/components/Shipping.js: -------------------------------------------------------------------------------- 1 | import useFormContext from "../hooks/useFormContext" 2 | 3 | const Shipping = () => { 4 | 5 | const { data, handleChange } = useFormContext() 6 | 7 | const content = ( 8 | <> 9 | 19 | 20 |
21 |
22 | 23 | 33 |
34 |
35 | 36 | 46 |
47 |
48 | 49 | 50 | 60 | 61 | 62 | 72 | 73 | 74 | 84 | 85 | 86 | 145 | 146 | 147 | 158 | 159 | ) 160 | 161 | return content 162 | } 163 | export default Shipping -------------------------------------------------------------------------------- /src/context/FormContext.js: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useEffect } from "react" 2 | 3 | const FormContext = createContext({}) 4 | 5 | export const FormProvider = ({ children }) => { 6 | 7 | const title = { 8 | 0: 'Billing Info', 9 | 1: 'Shipping Info', 10 | 2: 'Opt-In' 11 | } 12 | 13 | const [page, setPage] = useState(0) 14 | 15 | const [data, setData] = useState({ 16 | billFirstName: "", 17 | billLastName: "", 18 | billAddress1: "", 19 | billAddress2: "", 20 | billCity: "", 21 | billState: "", 22 | billZipCode: "", 23 | sameAsBilling: false, 24 | shipFirstName: "", 25 | shipLastName: "", 26 | shipAddress1: "", 27 | shipAddress2: "", 28 | shipCity: "", 29 | shipState: "", 30 | shipZipCode: "", 31 | optInNews: false 32 | }) 33 | 34 | 35 | useEffect(() => { 36 | if (data.sameAsBilling) { 37 | setData(prevData => ({ 38 | ...prevData, 39 | shipFirstName: prevData.billFirstName, 40 | shipLastName: prevData.billLastName, 41 | shipAddress1: prevData.billAddress1, 42 | shipAddress2: prevData.billAddress2, 43 | shipCity: prevData.billCity, 44 | shipState: prevData.billState, 45 | shipZipCode: prevData.billZipCode 46 | })) 47 | } else { 48 | setData(prevData => ({ 49 | ...prevData, 50 | shipFirstName: "", 51 | shipLastName: "", 52 | shipAddress1: "", 53 | shipAddress2: "", 54 | shipCity: "", 55 | shipState: "", 56 | shipZipCode: "" 57 | })) 58 | } 59 | }, [data.sameAsBilling]) 60 | 61 | 62 | const handleChange = e => { 63 | const type = e.target.type 64 | 65 | const name = e.target.name 66 | 67 | const value = type === "checkbox" 68 | ? e.target.checked 69 | : e.target.value 70 | 71 | setData(prevData => ({ 72 | ...prevData, 73 | [name]: value 74 | })) 75 | } 76 | 77 | const { 78 | billAddress2, 79 | sameAsBilling, 80 | shipAddress2, 81 | optInNews, 82 | ...requiredInputs } = data 83 | 84 | const canSubmit = [...Object.values(requiredInputs)].every(Boolean) && page === Object.keys(title).length - 1 85 | 86 | const canNextPage1 = Object.keys(data) 87 | .filter(key => key.startsWith('bill') && key !== 'billAddress2') 88 | .map(key => data[key]) 89 | .every(Boolean) 90 | 91 | const canNextPage2 = Object.keys(data) 92 | .filter(key => key.startsWith('ship') && key !== 'shipAddress2') 93 | .map(key => data[key]) 94 | .every(Boolean) 95 | 96 | const disablePrev = page === 0 97 | 98 | const disableNext = 99 | (page === Object.keys(title).length - 1) 100 | || (page === 0 && !canNextPage1) 101 | || (page === 1 && !canNextPage2) 102 | 103 | const prevHide = page === 0 && "remove-button" 104 | 105 | const nextHide = page === Object.keys(title).length - 1 && "remove-button" 106 | 107 | const submitHide = page !== Object.keys(title).length - 1 && "remove-button" 108 | 109 | return ( 110 | 111 | {children} 112 | 113 | ) 114 | } 115 | 116 | export default FormContext -------------------------------------------------------------------------------- /src/hooks/useFormContext.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import FormContext from "../context/FormContext" 3 | 4 | const useFormContext = () => { 5 | return useContext(FormContext) 6 | } 7 | 8 | export default useFormContext -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Nunito&display=swap'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | /* || UTILITY CLASSES */ 10 | 11 | .remove-button { 12 | display: none; 13 | } 14 | 15 | .offscreen { 16 | position: absolute; 17 | left: -10000px; 18 | } 19 | 20 | .split-container { 21 | display: flex; 22 | flex-flow: row nowrap; 23 | justify-content: space-evenly; 24 | gap: 1rem; 25 | } 26 | 27 | .flex-col { 28 | display: flex; 29 | flex-flow: column nowrap; 30 | gap: 0.35rem; 31 | width: 100%; 32 | } 33 | 34 | input:invalid { 35 | outline: 2px solid red; 36 | background-color: rgb(255, 255, 139); 37 | } 38 | 39 | 40 | /* || GENERAL STYLES */ 41 | 42 | body { 43 | font-family: 'Nunito', sans-serif; 44 | min-height: 100vh; 45 | background: radial-gradient(skyblue, darkblue); 46 | padding: 1rem; 47 | } 48 | 49 | input, 50 | button, 51 | textarea, 52 | select { 53 | font: inherit; 54 | } 55 | 56 | #root { 57 | display: flex; 58 | flex-flow: column nowrap; 59 | gap: 0.5rem; 60 | } 61 | 62 | .progress-container { 63 | width: 100%; 64 | max-width: 600px; 65 | margin: auto; 66 | padding: 1rem; 67 | border: 1px solid #000; 68 | border-radius: 10px; 69 | background-color: whitesmoke; 70 | position: relative; 71 | } 72 | 73 | .barmarker-container { 74 | display: flex; 75 | flex-flow: row nowrap; 76 | justify-content: space-around; 77 | } 78 | 79 | .barmarker { 80 | width: 100%; 81 | display: grid; 82 | place-content: center; 83 | } 84 | 85 | .barmarker:is(:not(:last-child)) { 86 | border-right: 1px solid #000; 87 | } 88 | 89 | .progress { 90 | -webkit-appearance: none; 91 | appearance: none; 92 | width: 100%; 93 | height: 32px; 94 | margin-top: 5px; 95 | } 96 | 97 | .progress::-webkit-progress-bar { 98 | background-color: lightcoral; 99 | border-radius: 10px; 100 | } 101 | 102 | .progress::-webkit-progress-value { 103 | background-color: limegreen; 104 | border-radius: 10px; 105 | } 106 | 107 | .progress::after { 108 | content: attr(value) '%'; 109 | position: absolute; 110 | width: 56px; 111 | top: 53%; 112 | left: 50%; 113 | margin-left: -28px; 114 | font-weight: bold; 115 | } 116 | 117 | .form { 118 | width: 100%; 119 | max-width: 600px; 120 | margin: auto; 121 | padding: 0.75rem; 122 | border: 1px solid #000; 123 | border-radius: 10px; 124 | background-color: whitesmoke; 125 | } 126 | 127 | .form-header { 128 | display: flex; 129 | flex-flow: row nowrap; 130 | justify-content: space-between; 131 | align-items: center; 132 | } 133 | 134 | .button-container { 135 | display: flex; 136 | flex-flow: row nowrap; 137 | gap: 0.5rem; 138 | } 139 | 140 | .button { 141 | padding: 0.5rem; 142 | border-radius: 10px; 143 | } 144 | 145 | .button:focus { 146 | outline: 2px solid gold; 147 | } 148 | 149 | input, 150 | select { 151 | padding: 0.5rem; 152 | border-radius: 10px; 153 | width: 100%; 154 | } 155 | 156 | input[type="checkbox"] { 157 | width: 16px; 158 | height: 16px; 159 | margin-right: 0.5rem; 160 | } 161 | 162 | ul { 163 | list-style-type: none; 164 | margin-bottom: 1rem; 165 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | --------------------------------------------------------------------------------