├── src ├── components │ ├── App │ │ ├── index.js │ │ ├── App.css │ │ └── App.js │ ├── Menu │ │ ├── index.js │ │ ├── Menu.css │ │ └── Menu.js │ ├── Footer │ │ ├── index.js │ │ ├── Footer.js │ │ └── Footer.css │ ├── Option │ │ ├── index.js │ │ ├── Option.js │ │ └── Option.css │ ├── Preview │ │ ├── index.js │ │ ├── Preview.css │ │ └── Preview.js │ ├── Summary │ │ ├── index.js │ │ ├── Summary.css │ │ └── Summary.js │ ├── Settings │ │ ├── index.js │ │ ├── Settings.css │ │ └── Settings.js │ ├── Slideshow │ │ ├── index.js │ │ ├── Slideshow.js │ │ └── Slideshow.css │ └── InteriorPreview │ │ ├── index.js │ │ ├── InteriorPreview.css │ │ └── InteriorPreview.js ├── index.js ├── utils.js ├── index.css └── data.js ├── public ├── robots.txt ├── favicon.ico ├── square_logo192.png ├── square_logo512.png ├── interiors │ ├── cream.jpeg │ ├── all_black.jpeg │ └── black_and_white.jpeg ├── wheels │ ├── model_3 │ │ ├── model_3_wheel_1.png │ │ └── model_3_wheel_2.png │ ├── model_s │ │ ├── model_s_wheel_1.png │ │ └── model_s_wheel_2.png │ ├── model_x │ │ ├── model_x_wheel_1.png │ │ └── model_x_wheel_2.png │ └── model_y │ │ ├── model_y_wheel_1.png │ │ └── model_y_wheel_2.png ├── cars │ ├── model_3 │ │ ├── model_3_red_wheel_1.png │ │ ├── model_3_red_wheel_2.png │ │ ├── model_3_black_wheel_1.png │ │ ├── model_3_black_wheel_2.png │ │ ├── model_3_blue_wheel_1.png │ │ ├── model_3_blue_wheel_2.png │ │ ├── model_3_silver_wheel_1.png │ │ ├── model_3_silver_wheel_2.png │ │ ├── model_3_white_wheel_1.png │ │ └── model_3_white_wheel_2.png │ ├── model_s │ │ ├── model_s_red_wheel_1.png │ │ ├── model_s_red_wheel_2.png │ │ ├── model_s_black_wheel_1.png │ │ ├── model_s_black_wheel_2.png │ │ ├── model_s_blue_wheel_1.png │ │ ├── model_s_blue_wheel_2.png │ │ ├── model_s_silver_wheel_1.png │ │ ├── model_s_silver_wheel_2.png │ │ ├── model_s_white_wheel_1.png │ │ └── model_s_white_wheel_2.png │ ├── model_x │ │ ├── model_x_red_wheel_1.png │ │ ├── model_x_red_wheel_2.png │ │ ├── model_x_black_wheel_1.png │ │ ├── model_x_black_wheel_2.png │ │ ├── model_x_blue_wheel_1.png │ │ ├── model_x_blue_wheel_2.png │ │ ├── model_x_silver_wheel_1.png │ │ ├── model_x_silver_wheel_2.png │ │ ├── model_x_white_wheel_1.png │ │ └── model_x_white_wheel_2.png │ └── model_y │ │ ├── model_y_red_wheel_1.png │ │ ├── model_y_red_wheel_2.png │ │ ├── model_y_black_wheel_1.png │ │ ├── model_y_black_wheel_2.png │ │ ├── model_y_blue_wheel_1.png │ │ ├── model_y_blue_wheel_2.png │ │ ├── model_y_silver_wheel_1.png │ │ ├── model_y_silver_wheel_2.png │ │ ├── model_y_white_wheel_1.png │ │ └── model_y_white_wheel_2.png ├── manifest.json ├── logo_square.svg ├── index.html ├── logo.svg └── logo_white.svg ├── .gitignore ├── package.json ├── LICENSE └── README.md /src/components/App/index.js: -------------------------------------------------------------------------------- 1 | import App from './App'; 2 | export default App; -------------------------------------------------------------------------------- /src/components/Menu/index.js: -------------------------------------------------------------------------------- 1 | import Menu from './Menu'; 2 | export default Menu; -------------------------------------------------------------------------------- /src/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import Footer from './Footer'; 2 | export default Footer; -------------------------------------------------------------------------------- /src/components/Option/index.js: -------------------------------------------------------------------------------- 1 | import Option from './Option'; 2 | export default Option; -------------------------------------------------------------------------------- /src/components/Preview/index.js: -------------------------------------------------------------------------------- 1 | import Preview from './Preview'; 2 | export default Preview; -------------------------------------------------------------------------------- /src/components/Summary/index.js: -------------------------------------------------------------------------------- 1 | import Summary from './Summary'; 2 | export default Summary; -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/components/Settings/index.js: -------------------------------------------------------------------------------- 1 | import Settings from './Settings'; 2 | export default Settings; -------------------------------------------------------------------------------- /src/components/Slideshow/index.js: -------------------------------------------------------------------------------- 1 | import Slideshow from './Slideshow'; 2 | export default Slideshow; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/square_logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/square_logo192.png -------------------------------------------------------------------------------- /public/square_logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/square_logo512.png -------------------------------------------------------------------------------- /src/components/InteriorPreview/index.js: -------------------------------------------------------------------------------- 1 | import InteriorPreview from './InteriorPreview'; 2 | export default InteriorPreview; -------------------------------------------------------------------------------- /public/interiors/cream.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/interiors/cream.jpeg -------------------------------------------------------------------------------- /public/interiors/all_black.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/interiors/all_black.jpeg -------------------------------------------------------------------------------- /public/interiors/black_and_white.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/interiors/black_and_white.jpeg -------------------------------------------------------------------------------- /public/wheels/model_3/model_3_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/wheels/model_3/model_3_wheel_1.png -------------------------------------------------------------------------------- /public/wheels/model_3/model_3_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/wheels/model_3/model_3_wheel_2.png -------------------------------------------------------------------------------- /public/wheels/model_s/model_s_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/wheels/model_s/model_s_wheel_1.png -------------------------------------------------------------------------------- /public/wheels/model_s/model_s_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/wheels/model_s/model_s_wheel_2.png -------------------------------------------------------------------------------- /public/wheels/model_x/model_x_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/wheels/model_x/model_x_wheel_1.png -------------------------------------------------------------------------------- /public/wheels/model_x/model_x_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/wheels/model_x/model_x_wheel_2.png -------------------------------------------------------------------------------- /public/wheels/model_y/model_y_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/wheels/model_y/model_y_wheel_1.png -------------------------------------------------------------------------------- /public/wheels/model_y/model_y_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/wheels/model_y/model_y_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_3/model_3_red_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_3/model_3_red_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_3/model_3_red_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_3/model_3_red_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_s/model_s_red_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_s/model_s_red_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_s/model_s_red_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_s/model_s_red_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_x/model_x_red_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_x/model_x_red_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_x/model_x_red_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_x/model_x_red_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_y/model_y_red_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_y/model_y_red_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_y/model_y_red_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_y/model_y_red_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_3/model_3_black_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_3/model_3_black_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_3/model_3_black_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_3/model_3_black_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_3/model_3_blue_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_3/model_3_blue_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_3/model_3_blue_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_3/model_3_blue_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_3/model_3_silver_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_3/model_3_silver_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_3/model_3_silver_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_3/model_3_silver_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_3/model_3_white_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_3/model_3_white_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_3/model_3_white_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_3/model_3_white_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_s/model_s_black_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_s/model_s_black_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_s/model_s_black_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_s/model_s_black_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_s/model_s_blue_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_s/model_s_blue_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_s/model_s_blue_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_s/model_s_blue_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_s/model_s_silver_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_s/model_s_silver_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_s/model_s_silver_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_s/model_s_silver_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_s/model_s_white_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_s/model_s_white_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_s/model_s_white_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_s/model_s_white_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_x/model_x_black_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_x/model_x_black_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_x/model_x_black_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_x/model_x_black_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_x/model_x_blue_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_x/model_x_blue_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_x/model_x_blue_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_x/model_x_blue_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_x/model_x_silver_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_x/model_x_silver_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_x/model_x_silver_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_x/model_x_silver_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_x/model_x_white_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_x/model_x_white_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_x/model_x_white_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_x/model_x_white_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_y/model_y_black_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_y/model_y_black_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_y/model_y_black_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_y/model_y_black_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_y/model_y_blue_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_y/model_y_blue_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_y/model_y_blue_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_y/model_y_blue_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_y/model_y_silver_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_y/model_y_silver_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_y/model_y_silver_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_y/model_y_silver_wheel_2.png -------------------------------------------------------------------------------- /public/cars/model_y/model_y_white_wheel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_y/model_y_white_wheel_1.png -------------------------------------------------------------------------------- /public/cars/model_y/model_y_white_wheel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlterClassIO/react-car-configurator/HEAD/public/cars/model_y/model_y_white_wheel_2.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './components/App'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function formatNumber(value) { 2 | return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 3 | }; 4 | 5 | export function formatPrice(value, zero = "included") { 6 | if (isNaN(value)) return null; 7 | return value === 0 ? zero : `$${formatNumber(value)}`; 8 | }; 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | min-width: 100%; 3 | width: 100%; 4 | min-height: 100%; 5 | height: 100%; 6 | overflow: hidden; 7 | } 8 | 9 | .app-content { 10 | width: 100%; 11 | height: calc(100% - var(--menu-height) - var(--footer-height)); 12 | margin-top: var(--menu-height); 13 | display: flex; 14 | flex-direction: column; 15 | overflow-y: auto; 16 | overflow-x: hidden; 17 | } 18 | 19 | @media(min-width: 992px) { 20 | .app-content { 21 | flex-direction: row; 22 | justify-content: center; 23 | overflow-y: hidden; 24 | } 25 | } -------------------------------------------------------------------------------- /src/components/InteriorPreview/InteriorPreview.css: -------------------------------------------------------------------------------- 1 | .interior-preview { 2 | border-right: 1px solid var(--theme-border); 3 | flex: 4; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | align-items: flex-start; 8 | min-height: 50%; 9 | max-height: 100%; 10 | width: 100%; 11 | overflow: hidden; 12 | } 13 | 14 | .interior-preview > svg { 15 | width: 100%; 16 | height: 100%; 17 | overflow: hidden; 18 | } 19 | 20 | @media only screen 21 | and (orientation: landscape) 22 | and (max-width: 992px) { 23 | .interior-preview { 24 | min-height: 100%; 25 | } 26 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Car Configurator", 3 | "name": "Car Configurator by AlterClass", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "square_logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "square_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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-car-configurator", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "react": "^17.0.1", 6 | "react-dom": "^17.0.1", 7 | "react-icons": "^4.2.0", 8 | "react-scripts": "^4.0.3" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 AlterClass 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/InteriorPreview/InteriorPreview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | // Styles 4 | import './InteriorPreview.css'; 5 | 6 | /* 7 | * TODO 8 | * 9 | * Requirements: 10 | * - use React hooks if needed 11 | * - use performance optimization if needed 12 | * 13 | */ 14 | const InteriorPreview = ({ interior = null }) => { 15 | return ( 16 |
17 | 21 | 22 | 31 | 32 |
33 | ); 34 | }; 35 | 36 | InteriorPreview.propTypes = { 37 | interior: PropTypes.shape({ 38 | label: PropTypes.string, 39 | value: PropTypes.string 40 | }) 41 | }; 42 | 43 | export default InteriorPreview; 44 | -------------------------------------------------------------------------------- /src/components/Summary/Summary.css: -------------------------------------------------------------------------------- 1 | .summary { 2 | padding: 32px; 3 | box-sizing: border-box; 4 | text-align: center; 5 | } 6 | .summary > h1 { 7 | padding: 0; 8 | margin: 0; 9 | } 10 | 11 | .summary-edd { 12 | text-decoration: underline; 13 | } 14 | 15 | .summary-content { 16 | text-align: left; 17 | margin-top: 32px; 18 | } 19 | .summary-content > p { 20 | font-weight: 500; 21 | margin: 0; 22 | padding: 8px 0; 23 | display: flex; 24 | justify-content: space-between; 25 | } 26 | .summary-content > p:first-child { 27 | font-size: 22px; 28 | border-bottom: 1px solid var(--theme-separator); 29 | } 30 | .summary-content > p:last-child { 31 | font-size: 16px; 32 | border-top: 1px solid var(--theme-separator); 33 | } 34 | .summary-content > ul { 35 | margin: 16px 0; 36 | padding: 0; 37 | list-style: none; 38 | font-size: 16px; 39 | } 40 | .summary-content > ul > li { 41 | padding: 8px 0; 42 | display: flex; 43 | justify-content: space-between; 44 | } 45 | .summary-content > ul > li > span:last-child { 46 | padding-left: 12px; 47 | text-transform: capitalize; 48 | opacity: var(--theme-medium-opacity); 49 | } 50 | 51 | @media(min-width: 992px) { 52 | .summary { 53 | flex: 1; 54 | overflow-y: auto; 55 | min-width: 435px; 56 | padding: 48px; 57 | } 58 | } -------------------------------------------------------------------------------- /src/components/Preview/Preview.css: -------------------------------------------------------------------------------- 1 | .preview { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | position: relative; 7 | padding: 0 0 32px 0; 8 | box-sizing: border-box; 9 | overflow: hidden; 10 | flex-shrink: 0; 11 | } 12 | 13 | .specs { 14 | max-width: 650px; 15 | width: 100%; 16 | margin: 0; 17 | padding: 0 32px; 18 | list-style: none; 19 | display: flex; 20 | justify-content: center; 21 | align-items: flex-start; 22 | box-sizing: border-box; 23 | } 24 | .specs > li:not(:last-child) { 25 | border-right: 1px solid var(--theme-separator); 26 | } 27 | .specs > li { 28 | text-align: center; 29 | display: flex; 30 | flex-direction: column; 31 | padding: 0 12px; 32 | } 33 | .specs > li:first-child { 34 | padding-left: 0; 35 | } 36 | .specs > li:last-child { 37 | padding-right: 0; 38 | } 39 | .specs > li > span.specs-value { 40 | font-size: 1.5rem; 41 | } 42 | .specs > li > span.specs-label { 43 | font-size: 1rem; 44 | padding-top: 8px; 45 | } 46 | 47 | @media(min-width: 576px) { 48 | .specs > li { 49 | padding: 0 32px; 50 | } 51 | .specs > li > span.specs-value { 52 | font-size: 2.5rem; 53 | } 54 | } 55 | @media(min-width: 992px) { 56 | .preview { 57 | flex: 4; 58 | border-right: 1px solid var(--theme-separator); 59 | } 60 | } -------------------------------------------------------------------------------- /src/components/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { formatPrice } from '../../utils'; 4 | // Styles 5 | import './Footer.css'; 6 | // Icons 7 | import { MdNavigateBefore, MdNavigateNext } from 'react-icons/md'; 8 | 9 | /* 10 | * TODO 11 | * 12 | * Requirements: 13 | * - use React hooks if needed 14 | * - use performance optimization if needed 15 | * 16 | */ 17 | const Footer = ({ 18 | totalPrice = 0, 19 | disablePrev = true, 20 | disableNext = true, 21 | onClickPrev = () => null, 22 | onClickNext = () => null 23 | }) => ( 24 |
25 |
26 | 33 |
34 |
35 | {formatPrice(totalPrice, '-')} 36 |
37 |
38 | 45 |
46 |
47 | ); 48 | 49 | Footer.propTypes = { 50 | totalPrice: PropTypes.number, 51 | disablePrev: PropTypes.bool, 52 | disableNext: PropTypes.bool, 53 | onClickPrev: PropTypes.func, 54 | onClickNext: PropTypes.func 55 | }; 56 | 57 | export default Footer; 58 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | height: 100%; 3 | margin: 0; 4 | overflow: hidden; 5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | scroll-behavior: smooth; 11 | } 12 | 13 | body { 14 | color: var(--theme-text); 15 | background: var(--theme-background); 16 | transition: all .25s ease-in-out; 17 | 18 | --theme-background: #ffffff; 19 | --theme-footer-background: #333; 20 | --theme-surface: transparent; 21 | --theme-text: #151618; 22 | --theme-footer-text: #ffffff; 23 | --theme-primary: #c33364; 24 | --theme-separator: #dfdfdf; 25 | --theme-footer-separator: #333; 26 | --theme-border: #ddd; 27 | --theme-surface-border: #ddd; 28 | 29 | --theme-active-opacity: 1; 30 | --theme-medium-opacity: 0.60; 31 | --theme-disabled-opacity: 0.38; 32 | 33 | --theme-drop-shadow: none; 34 | 35 | --menu-height: 67px; 36 | --footer-height: 72px; 37 | } 38 | 39 | body.dark-mode { 40 | --theme-background: #151618; 41 | --theme-footer-background: #151618; 42 | --theme-surface: #25282c; 43 | --theme-text: #ffffff; 44 | --theme-footer-text: #ffffff; 45 | --theme-primary: #c33364; 46 | --theme-separator: rgba(244,245,246,0.12); 47 | --theme-footer-separator: rgba(244,245,246,0.12); 48 | --theme-border: #151618; 49 | --theme-surface-border: rgba(244,245,246,0.12); 50 | --theme-drop-shadow: drop-shadow(0 0 0.75rem var(--theme-primary)); 51 | } -------------------------------------------------------------------------------- /public/logo_square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/components/Slideshow/Slideshow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | // Styles 4 | import './Slideshow.css'; 5 | 6 | /* 7 | * TODO 8 | * 9 | * Requirements: 10 | * - use React hooks if needed 11 | * - use performance optimization if needed 12 | * 13 | */ 14 | const Slideshow = ({ 15 | items = [], 16 | index = 0, 17 | showPrev = true, 18 | showNext = true, 19 | onClickPrev = () => null, 20 | onClickNext = () => null 21 | }) => ( 22 |
23 | { 24 | items.map((item, i) => ( 25 |
33 | {item.alt} 38 |
39 | )) 40 | } 41 | {showPrev 42 | ? ( 43 | 47 | ) : null 48 | } 49 | {showNext 50 | ? ( 51 | 55 | ) : null 56 | } 57 |
58 | ); 59 | 60 | Slideshow.propTypes = { 61 | items: PropTypes.arrayOf( 62 | PropTypes.exact({ 63 | alt: PropTypes.string, 64 | url: PropTypes.string, 65 | scale: PropTypes.bool 66 | }) 67 | ), 68 | index: PropTypes.number, 69 | showPrev: PropTypes.bool, 70 | showNext: PropTypes.bool, 71 | onClickPrev: PropTypes.func, 72 | onClickNext: PropTypes.func 73 | }; 74 | 75 | export default Slideshow; 76 | -------------------------------------------------------------------------------- /src/components/Option/Option.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | // Styles 4 | import './Option.css'; 5 | 6 | const types = ["text", "color", "image"]; 7 | 8 | /* 9 | * TODO: Leverage memoization with Option 10 | * 11 | * Tips: 12 | * - Wrap the Option component by using the React.memo HoC 13 | * - Don't forget to use the useCallback hook to wrap any event handlers/callbacks form the parent component 14 | * 15 | */ 16 | const Option = ({ 17 | value = '', 18 | label = '', 19 | src = '', 20 | type = '', 21 | price = '', 22 | active = false, 23 | onSelectOption = () => null 24 | }) => { 25 | if (!types.includes(type)) return null; 26 | 27 | let classNames = `option ${type}-option`; 28 | if (active) { 29 | classNames += ' active'; 30 | } 31 | 32 | const renderContent = () => { 33 | switch(type) { 34 | case "text": 35 | return ( 36 | <> 37 | {label} 38 | {price ? {price} : null} 39 | 40 | ); 41 | case "image": 42 | return {label}; 43 | case "color": 44 | return
; 45 | default: 46 | return null; 47 | } 48 | } 49 | 50 | return ( 51 |
onSelectOption(value)} 55 | > 56 | {renderContent()} 57 |
58 | ); 59 | }; 60 | 61 | Option.propTypes = { 62 | value: PropTypes.string, 63 | label: PropTypes.string, 64 | type: PropTypes.oneOf(types), 65 | price: PropTypes.string, 66 | active: PropTypes.bool, 67 | onSelectOption: PropTypes.func 68 | }; 69 | 70 | export default Option; 71 | -------------------------------------------------------------------------------- /src/components/Menu/Menu.css: -------------------------------------------------------------------------------- 1 | .menu-container { 2 | position: fixed; 3 | top: 0; 4 | width: 100%; 5 | padding: 0 32px; 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | box-sizing: border-box; 10 | z-index: 99; 11 | height: var(--menu-height); 12 | box-sizing: border-box; 13 | border-bottom: 1px solid var(--theme-separator); 14 | } 15 | 16 | .menu-container .logo { 17 | display: block; 18 | } 19 | .menu-container .logo > img { 20 | width: 150px; 21 | } 22 | 23 | .menu-nav { 24 | display: none; 25 | flex: 1; 26 | margin: 0; 27 | padding: 0; 28 | height: 100%; 29 | list-style-type: none; 30 | list-style-position: inside; 31 | counter-reset: menuCounter; 32 | } 33 | .menu-nav > li { 34 | counter-increment: menuCounter; 35 | flex-grow: 1; 36 | text-align: center; 37 | text-transform: capitalize; 38 | max-width: 225px; 39 | opacity: var(--theme-disabled-opacity); 40 | cursor: pointer; 41 | box-sizing: border-box; 42 | transition: opacity .15s ease-in-out; 43 | } 44 | .menu-nav > li.selected { 45 | box-shadow: inset 0 -5px 0 -1px var(--theme-primary); 46 | opacity: var(--theme-active-opacity); 47 | } 48 | .menu-nav > li:hover { 49 | opacity: var(--theme-active-opacity); 50 | } 51 | .menu-nav > li::before { 52 | content: counter(menuCounter) "."; 53 | padding-right: 4px; 54 | font-size: 16px; 55 | font-weight: 600; 56 | } 57 | .menu-nav > li > h2 { 58 | display: inline-block; 59 | margin: 0; 60 | padding: 24px 0; 61 | font-weight: 600; 62 | font-size: 16px; 63 | } 64 | 65 | .mode-icon { 66 | min-width: 24px; 67 | min-height: 24px; 68 | cursor: pointer; 69 | } 70 | 71 | @media(min-width: 992px) { 72 | .menu-container { 73 | justify-content: flex-start; 74 | } 75 | .menu-nav { 76 | display: flex; 77 | justify-content: center; 78 | } 79 | } -------------------------------------------------------------------------------- /src/components/Menu/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | // Styles 4 | import './Menu.css'; 5 | // Icons 6 | import { FaMoon, FaSun } from 'react-icons/fa'; 7 | 8 | /* 9 | * TODO: Refactor Menu as a functional component 10 | * 11 | * Requirements: 12 | * - Create a custom hook to implement dark mode named useDarkMode 13 | * - Switch from setState to the useDarkMode hook 14 | * - Use function closures instead of this for callbacks and event handlers 15 | * - Menu logic and behavior should remain the same 16 | * 17 | */ 18 | class Menu extends React.Component { 19 | state = { 20 | darkMode: false, 21 | }; 22 | 23 | handleOnChangeMode = () => { 24 | this.setState(prevState => ({ 25 | ...prevState, 26 | darkMode: !prevState.darkMode, 27 | })); 28 | }; 29 | 30 | render() { 31 | const ModeIcon = this.state.darkMode ? FaSun : FaMoon; 32 | 33 | const brandLogo = this.state.darkMode 34 | ? `${process.env.PUBLIC_URL}/logo_white.svg` 35 | : `${process.env.PUBLIC_URL}/logo.svg`; 36 | 37 | return ( 38 |
39 | 40 | AlterClass 41 | 42 | 53 | 54 |
55 | ); 56 | } 57 | } 58 | 59 | Menu.propTypes = { 60 | items: PropTypes.arrayOf(PropTypes.string), 61 | selectedItem: PropTypes.number, 62 | onSelectItem: PropTypes.func, 63 | }; 64 | 65 | export default Menu; 66 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | position: fixed; 3 | bottom: 0; 4 | width: 100%; 5 | z-index: 99; 6 | height: var(--footer-height); 7 | box-sizing: border-box; 8 | display: flex; 9 | justify-content: space-between; 10 | align-items: center; 11 | font-size: 18px; 12 | font-weight: 400; 13 | padding: 0 32px; 14 | border-top: 1px solid var(--theme-footer-separator); 15 | color: var(--theme-footer-text); 16 | background: var(--theme-footer-background); 17 | } 18 | 19 | .footer > * { 20 | flex: 1; 21 | } 22 | 23 | .footer > *:first-child { 24 | display: flex; 25 | justify-content: flex-start; 26 | } 27 | .footer > *:nth-child(2) { 28 | display: flex; 29 | justify-content: center; 30 | padding: 0 12px; 31 | } 32 | .footer > *:last-child { 33 | display: flex; 34 | justify-content: flex-end; 35 | } 36 | 37 | .footer button { 38 | text-align: right; 39 | cursor: pointer; 40 | background: var(--theme-primary); 41 | color: white; 42 | font-weight: 600; 43 | font-size: 14px; 44 | text-transform: uppercase; 45 | line-height: 24px; 46 | outline: none; 47 | border: 2px solid transparent; 48 | border-radius: 25px; 49 | display: inline-flex; 50 | justify-content: center; 51 | align-items: center; 52 | max-width: 75px; 53 | padding: 6px; 54 | box-sizing: border-box; 55 | transition: all .2s ease; 56 | } 57 | .footer button:focus, 58 | .footer button:active { 59 | box-shadow: inset 0 0 0 2px white; 60 | } 61 | .footer button:disabled { 62 | cursor: auto; 63 | opacity: 0.38; 64 | } 65 | 66 | .footer button > span { 67 | display: none; 68 | } 69 | .footer button > svg { 70 | min-width: 24px; 71 | min-height: 24px; 72 | } 73 | 74 | @media(min-width: 576px) { 75 | .footer { 76 | font-size: 24px; 77 | } 78 | .footer button { 79 | width: 100%; 80 | max-width: 300px; 81 | } 82 | .footer button > span { 83 | display: inline-block; 84 | } 85 | .footer button > svg { 86 | display: none; 87 | } 88 | } -------------------------------------------------------------------------------- /src/components/Settings/Settings.css: -------------------------------------------------------------------------------- 1 | .settings { 2 | padding: 32px; 3 | box-sizing: border-box; 4 | text-align: center; 5 | } 6 | 7 | .settings-group { 8 | max-width: 450px; 9 | margin: 0 auto; 10 | margin-bottom: 32px; 11 | } 12 | .settings-group > h3 { 13 | text-transform: capitalize; 14 | font-weight: 400; 15 | font-size: 24px; 16 | margin-top: 0; 17 | margin-bottom: 16px; 18 | } 19 | 20 | .settings-group-disclaimer { 21 | font-size: 14px; 22 | line-height: 24px; 23 | opacity: var(--theme-medium-opacity); 24 | } 25 | 26 | .settings-options { 27 | margin: 0; 28 | padding: 0; 29 | display: flex; 30 | justify-content: center; 31 | flex-wrap: wrap; 32 | } 33 | .settings-options .settings-options-text { 34 | flex-wrap: nowrap; 35 | flex-direction: column; 36 | align-items: center; 37 | width: 100%; 38 | } 39 | 40 | .settings-group-label { 41 | font-weight: 500; 42 | font-size: 14px; 43 | line-height: 24px; 44 | display: flex; 45 | justify-content: center; 46 | align-items: center; 47 | } 48 | .settings-group-label > span:first-child { 49 | padding-right: 12px; 50 | } 51 | .settings-group-label > span:last-child { 52 | padding-left: 12px; 53 | } 54 | .settings-group-label > .price { 55 | font-size: 14px; 56 | text-transform: capitalize; 57 | } 58 | 59 | .settings-group-benefits > p { 60 | font-weight: 500; 61 | } 62 | .settings-group-benefits > ul { 63 | font-size: 16px; 64 | line-height: 24px; 65 | text-align: left; 66 | } 67 | .settings-group-benefits > ul > li { 68 | margin-bottom: .6em; 69 | } 70 | 71 | @media(min-width: 992px) { 72 | .settings { 73 | flex: 1; 74 | overflow-y: auto; 75 | min-width: 435px; 76 | padding: 48px; 77 | text-align: left; 78 | } 79 | .settings-group-label { 80 | justify-content: space-between; 81 | } 82 | .settings-options { 83 | justify-content: flex-start; 84 | } 85 | .settings-options .settings-options-text { 86 | flex-direction: row; 87 | flex-wrap: wrap; 88 | } 89 | } -------------------------------------------------------------------------------- /src/components/Slideshow/Slideshow.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes fade { 2 | from { opacity: .4 } 3 | to { opacity: 1 } 4 | } 5 | @keyframes fade { 6 | from { opacity: .4 } 7 | to { opacity: 1 } 8 | } 9 | 10 | .slideshow { 11 | position: relative; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | width: 100%; 16 | max-height: 100%; 17 | overflow: hidden; 18 | user-select: none; 19 | } 20 | .slideshow .arrow { 21 | display: none; 22 | position: absolute; 23 | padding: 1.5rem; 24 | box-shadow: 1px -1px 0 1px var(--theme-primary) inset; 25 | -webkit-box-shadow: 2px -2px var(--theme-primary) inset; 26 | border: solid transparent; 27 | border-width: 0 0 2rem 2rem; 28 | cursor: pointer; 29 | opacity: .45; 30 | transition: all .2s ease-in-out; 31 | } 32 | .slideshow .arrow:hover { 33 | opacity: 1; 34 | box-shadow: 2px -2px 0 2px var(--theme-primary) inset; 35 | -webkit-box-shadow: 4px -4px var(--theme-primary) inset; 36 | } 37 | .slideshow .arrow-next { 38 | transform: translateY(-50%) rotate(225deg); 39 | top: 50%; 40 | right: 16px; 41 | } 42 | .slideshow .arrow-prev { 43 | transform: translateY(-50%) rotate(45deg); 44 | top: 50%; 45 | left: 16px; 46 | } 47 | 48 | .slideshow-slide { 49 | display: none; 50 | -webkit-animation-name: fade; 51 | -webkit-animation-duration: .5s; 52 | -webkit-animation-timing-function: ease-in-out; 53 | animation-name: fade; 54 | animation-duration: .5s; 55 | animation-timing-function: ease-in-out; 56 | } 57 | .slideshow-slide.active { 58 | display: flex; 59 | justify-content: center; 60 | align-items: center; 61 | } 62 | .slideshow-slide.active > img { 63 | max-width: 100%; 64 | max-height: 100%; 65 | filter: var(--theme-drop-shadow); 66 | } 67 | .slideshow-slide > img.scale { 68 | transform: scale(1.4); 69 | } 70 | 71 | @media(min-width: 576px) { 72 | .slideshow .arrow { 73 | display: block; 74 | } 75 | } 76 | @media(min-width: 992px) { 77 | .slideshow { 78 | height: 80%; 79 | } 80 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Car Configurator 2 | 3 | Starter code for the project in the React Hooks module of the [React course](https://www.alterclass.io/courses/react). 4 | 5 | Follow along with the [classroom](https://classroom.alterclass.io) lessons to complete the project, or attend a live-stream session with your instructor from the [Discord server](https://discord.com/channels/742753758450155662/748890194136137838). 6 | 7 | [![React Car Configurator by AlterClass](https://alterclass.s3.eu-west-3.amazonaws.com/react-car-configurator.png)](https://react-car-configurator.netlify.app/) 8 | 9 | Check out the live demo of the final result: [https://react-car-configurator.netlify.app/](https://react-car-configurator.netlify.app/). 10 | 11 | ## create-react-app 12 | 13 | This project uses the popular [create-react-app (CRA)](https://create-react-app.dev/) command to setup a modern React application. This way we can focus on the code itself, and not worry about configuring many build tools. 14 | 15 | The [package.json](https://github.com/AlterClassIO/react-car-configurator/blob/master/package.json) file provides four scripts: 16 | 17 | - `start`: Runs the app in the development mode. 18 | - `build`: Builds the app for production to the build folder. It correctly bundles React in production mode and optimizes the build for the best performance. 19 | - `test`: Launches the test runner in the interactive watch mode. 20 | - `eject`: Remove create-react-app build dependency from your project. 21 | 22 | ## Instructions 23 | 24 | 1. Clone the project repository: `git clone https://github.com/AlterClassIO/react-car-configurator` 25 | 26 | 2. Navigate to the project folder: `cd react-car-configurator` 27 | 28 | 3. Install the dependencies: `npm install` 29 | 30 | 4. Start the app in the development mode: `npm start` 31 | 32 | ![Compiled successfully!](https://alterclass.s3.eu-west-3.amazonaws.com/react-car-configurator-compiled.png) 33 | 34 | 5. Open [http://localhost:3000](http://localhost:3000) to view your React application in the browser 35 | 36 | ![React Car Configurator starting point](https://alterclass.s3.eu-west-3.amazonaws.com/react-car-configurator-starting-point.png) 37 | 38 | 6. Follow along with the lesson. 39 | 40 | 7. Implement the project. 41 | 42 | 8. Submit! 43 | -------------------------------------------------------------------------------- /src/components/Summary/Summary.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { formatPrice } from '../../utils'; 4 | // Styles 5 | import './Summary.css'; 6 | 7 | /* 8 | * TODO 9 | * 10 | * Requirements: 11 | * - use React hooks if needed 12 | * - use performance optimization if needed 13 | * 14 | */ 15 | const Summary = ({ 16 | config = null, 17 | models = null, 18 | totalPrice = 0 19 | }) => { 20 | const selectedModel = models?.find(model => model?.key === config?.model); 21 | const selectedType = selectedModel?.types?.find(type => type.value === config?.car_type); 22 | const selectedColor = selectedModel?.colors?.find(color => color.value === config?.color); 23 | const selectedWheels = selectedModel?.wheels?.find(wheels => wheels.value === config?.wheels); 24 | const selectedInteriorColor = selectedModel?.interiorColors?.find(interiorColor => interiorColor.value === config?.interior_color); 25 | const selectedInteriorLayout = selectedModel?.interiorLayouts?.find(interiorLayout => interiorLayout.value === config?.interior_layout); 26 | 27 | return ( 28 |
29 |

Your {selectedModel?.name}

30 |

Estimated delivery: 5-9 weeks

31 |
32 |

Summary

33 |
    34 |
  • 35 | {selectedModel?.name} {selectedType?.label} 36 | {formatPrice(selectedType?.price)} 37 |
  • 38 |
  • 39 | {selectedColor?.label} 40 | {formatPrice(selectedColor?.price)} 41 |
  • 42 |
  • 43 | {selectedWheels?.label} 44 | {formatPrice(selectedWheels?.price)} 45 |
  • 46 |
  • 47 | {selectedInteriorColor?.label} 48 | {formatPrice(selectedInteriorColor?.price)} 49 |
  • 50 |
  • 51 | {selectedInteriorLayout?.label} 52 | {formatPrice(selectedInteriorLayout?.price)} 53 |
  • 54 |
55 |

56 | Total price 57 | {formatPrice(totalPrice)} 58 |

59 |
60 |
61 | ); 62 | }; 63 | 64 | Summary.propTypes = { 65 | config: PropTypes.object, 66 | models: PropTypes.array, 67 | totalPrice: PropTypes.number 68 | }; 69 | 70 | export default Summary; 71 | -------------------------------------------------------------------------------- /src/components/Preview/Preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | // Styles 4 | import './Preview.css'; 5 | // Components 6 | import Slideshow from '../Slideshow'; 7 | 8 | /* 9 | * TODO: Refactor Preview as a functional component 10 | * 11 | * Requirements: 12 | * - Use React hooks if necessary 13 | * - Use function closures instead of this for callbacks and event handlers 14 | * - Preview logic and behavior should remain the same 15 | * 16 | */ 17 | class Preview extends React.Component { 18 | get index() { 19 | return this.props?.models.findIndex(model => 20 | model.key === this.props.config?.model 21 | ); 22 | }; 23 | 24 | get items() { 25 | return this.props.models.map(model => ({ 26 | alt: model.name, 27 | url: `${process.env.PUBLIC_URL}/cars/model_${model.key}/model_${model.key}_${this.props.config.color}_${this.props.config.wheels}.png`, 28 | scale: ['x'].includes(model.key) 29 | })); 30 | }; 31 | 32 | get selectedModel() { 33 | return this.props.models.find(model => 34 | model.key === this.props.config.model 35 | ); 36 | }; 37 | 38 | get selectedType() { 39 | return this.selectedModel?.types?.find(type => 40 | type.value === this.props.config.car_type 41 | ); 42 | }; 43 | 44 | get specs() { 45 | return this.selectedType?.specs; 46 | }; 47 | 48 | handleOnClickPrev = () => { 49 | const newIndex = this.index > 0 50 | ? this.index - 1 51 | : this.props.models.length - 1; 52 | this.props.onChangeModel(this.props.models?.[newIndex]?.key); 53 | }; 54 | 55 | handleOnClickNext = () => { 56 | const newIndex = this.index < this.props.models.length - 1 57 | ? this.index + 1 58 | : 0; 59 | this.props.onChangeModel(this.props.models?.[newIndex]?.key); 60 | }; 61 | 62 | render() { 63 | return ( 64 |
65 | 73 | { 74 | this.props.showSpecs ? ( 75 |
    76 |
  • 77 | {this.specs?.range ?? ' - '}mi 78 | Range (EPA est.) 79 |
  • 80 |
  • 81 | {this.specs?.top_speed ?? ' - '}mph 82 | Top Speed 83 |
  • 84 |
  • 85 | {this.specs?.acceleration_time ?? ' - '}s 86 | 0-60 mph 87 |
  • 88 |
89 | ) : null 90 | } 91 |
92 | ); 93 | }; 94 | }; 95 | 96 | Preview.propTypes = { 97 | config: PropTypes.object, 98 | models: PropTypes.array, 99 | showAllModels: PropTypes.bool, 100 | showSpecs: PropTypes.bool, 101 | onChangeModel: PropTypes.func 102 | }; 103 | 104 | export default Preview; 105 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Car Configurator by AlterClass 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 34 | 35 | 44 | 45 | 46 | 47 |
48 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/components/Settings/Settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { formatPrice } from '../../utils'; 4 | // Styles 5 | import './Settings.css'; 6 | // Components 7 | import Option from '../Option'; 8 | 9 | /* 10 | * TODO: Refactor Editor to leverage React hooks 11 | * 12 | * Requirements: 13 | * - store selectedOptions in React state using the useState hook 14 | * - initialize state using lazy initialization 15 | * - use other React hooks if needed 16 | * 17 | */ 18 | const Settings = ({ 19 | config = null, 20 | settings = null, 21 | onSelectOption = () => null 22 | }) => { 23 | 24 | const selectedOptions = settings?.reduce( 25 | (acc, setting) => ({ 26 | ...acc, 27 | [setting.prop]: setting.options.find(option => 28 | option.value === config[setting.prop] 29 | ) ?? [] 30 | }), 31 | {} 32 | ); 33 | 34 | return ( 35 |
36 | { 37 | settings?.map(setting => { 38 | if (!setting.options || setting.options.length === 0) { 39 | return null; 40 | } 41 | return ( 42 |
46 |

{setting.label}

47 | { 48 | setting.disclaimer_1 ? ( 49 |

50 | {setting.disclaimer_1} 51 |

52 | ) : null 53 | } 54 |
55 | { 56 | setting.options.map(option => ( 57 |
70 | { 71 | setting.type !== "text" ? ( 72 |
73 | {selectedOptions?.[setting.prop]?.label} 74 | 75 | {formatPrice(selectedOptions?.[setting.prop]?.price)} 76 | 77 |
78 | ) : null 79 | } 80 | { 81 | selectedOptions?.[setting.prop]?.benefits ? ( 82 |
83 |

Model {config.model.toUpperCase()} {selectedOptions[setting.prop].label} includes:

84 |
    85 | { 86 | selectedOptions?.[setting.prop]?.benefits?.map((benefit, i) => ( 87 |
  • 88 | {benefit} 89 |
  • 90 | )) 91 | } 92 |
93 |
94 | ) : null 95 | } 96 | { 97 | setting.disclaimer_2 ? ( 98 |

99 | {setting.disclaimer_2} 100 |

101 | ) : null 102 | } 103 |
104 | )}) 105 | } 106 |
107 | ); 108 | }; 109 | 110 | Settings.propTypes = { 111 | config: PropTypes.object, 112 | settings: PropTypes.arrayOf( 113 | PropTypes.shape({ 114 | label: PropTypes.string, 115 | type: PropTypes.string, 116 | prop: PropTypes.string, 117 | options: PropTypes.array 118 | }) 119 | ), 120 | onSelectOption: PropTypes.func 121 | }; 122 | 123 | export default Settings; 124 | -------------------------------------------------------------------------------- /src/components/Option/Option.css: -------------------------------------------------------------------------------- 1 | .option { 2 | cursor: pointer; 3 | display: flex; 4 | justify-content: space-between; 5 | align-items: center; 6 | } 7 | .option > .price { 8 | font-size: 14px; 9 | padding-left: 12px; 10 | } 11 | 12 | .text-option { 13 | background: var(--theme-surface); 14 | border: 1px solid var(--theme-surface-border); 15 | padding: 12px 20px; 16 | margin-bottom: 12px; 17 | border-radius: 25px; 18 | width: 100%; 19 | box-sizing: border-box; 20 | text-transform: capitalize; 21 | transition: all .25s ease-in-out; 22 | } 23 | .text-option > span:first-child { 24 | overflow: hidden; 25 | white-space: nowrap; 26 | text-overflow: ellipsis; 27 | } 28 | .text-option.active { 29 | border-color: var(--theme-primary); 30 | box-shadow: 0 0 0 2px var(--theme-primary); 31 | font-weight: 500; 32 | } 33 | 34 | .image-option { 35 | width: 75px; 36 | height: 75px; 37 | padding: 8px; 38 | margin-right: 12px; 39 | margin-bottom: 12px; 40 | border-radius: 12px; 41 | background: var(--theme-surface);; 42 | border: 2px solid var(--theme-surface-border); 43 | box-sizing: border-box; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | } 48 | .image-option.active { 49 | border: 2px solid var(--theme-primary); 50 | } 51 | .image-option > img { 52 | max-width: 100%; 53 | max-height: 100%; 54 | } 55 | 56 | .color-option { 57 | width: 48px; 58 | height: 48px; 59 | padding: 4px; 60 | margin-right: 0px; 61 | margin-bottom: 8px; 62 | border-radius: 50%; 63 | background: transparent; 64 | box-sizing: border-box; 65 | } 66 | .color-option.active { 67 | border: 2px solid var(--theme-primary); 68 | } 69 | .color-option > div { 70 | display: block; 71 | width: 100%; 72 | height: 100%; 73 | border-radius: 50%; 74 | border: 2px solid var(--theme-border); 75 | box-sizing: border-box; 76 | } 77 | .color-option > div.white { 78 | background: white; 79 | } 80 | .color-option > div.black { 81 | background: linear-gradient( 82 | to bottom right, 83 | hsl(0, 0%, 100%) 0%, 84 | hsl(0, 0%, 98.73%) 0.3%, 85 | hsl(0, 0%, 95.14%) 1.4%, 86 | hsl(0, 0%, 89.6%) 3.2%, 87 | hsl(0, 0%, 82.46%) 5.8%, 88 | hsl(0, 0%, 74.07%) 9.3%, 89 | hsl(0, 0%, 64.8%) 13.6%, 90 | hsl(0, 0%, 54.99%) 18.9%, 91 | hsl(0, 0%, 45.01%) 25.1%, 92 | hsl(0, 0%, 35.2%) 32.4%, 93 | hsl(0, 0%, 25.93%) 40.7%, 94 | hsl(0, 0%, 17.54%) 50.2%, 95 | hsl(0, 0%, 10.4%) 60.8%, 96 | hsl(0, 0%, 4.86%) 72.6%, 97 | hsl(0, 0%, 1.27%) 85.7%, 98 | hsl(0, 0%, 0%) 100% 99 | ); 100 | } 101 | .color-option > div.silver { 102 | background: linear-gradient( 103 | to bottom right, 104 | hsl(0, 0%, 100%) 0%, 105 | hsl(208.97, 6.61%, 98.9%) 0.3%, 106 | hsl(208.97, 6.61%, 95.82%) 1.4%, 107 | hsl(208.97, 6.61%, 91.05%) 3.2%, 108 | hsl(208.97, 6.61%, 84.9%) 5.8%, 109 | hsl(208.97, 6.61%, 77.68%) 9.3%, 110 | hsl(208.97, 6.61%, 69.7%) 13.6%, 111 | hsl(208.97, 6.61%, 61.26%) 18.9%, 112 | hsl(208.97, 6.61%, 52.66%) 25.1%, 113 | hsl(208.97, 8.33%, 44.22%) 32.4%, 114 | hsl(208.97, 11.62%, 36.24%) 40.7%, 115 | hsl(208.97, 16.16%, 29.02%) 50.2%, 116 | hsl(208.97, 22.27%, 22.87%) 60.8%, 117 | hsl(208.97, 29.88%, 18.1%) 72.6%, 118 | hsl(208.97, 37.38%, 15.02%) 85.7%, 119 | hsl(208.97, 40.85%, 13.92%) 100% 120 | ); 121 | } 122 | .color-option > div.blue { 123 | background: linear-gradient( 124 | to bottom right, 125 | hsl(0, 0%, 100%) 0%, 126 | hsl(222.86, 49.12%, 99.15%) 0.3%, 127 | hsl(222.86, 49.12%, 96.74%) 1.4%, 128 | hsl(222.86, 49.12%, 93.03%) 3.2%, 129 | hsl(222.86, 49.12%, 88.24%) 5.8%, 130 | hsl(222.86, 49.12%, 82.61%) 9.3%, 131 | hsl(222.86, 49.12%, 76.4%) 13.6%, 132 | hsl(222.86, 49.12%, 69.82%) 18.9%, 133 | hsl(222.86, 49.12%, 63.12%) 25.1%, 134 | hsl(222.86, 49.12%, 56.55%) 32.4%, 135 | hsl(222.86, 49.12%, 50.33%) 40.7%, 136 | hsl(222.86, 60.76%, 44.7%) 50.2%, 137 | hsl(222.86, 73.94%, 39.92%) 60.8%, 138 | hsl(222.86, 86.58%, 36.2%) 72.6%, 139 | hsl(222.86, 96.23%, 33.8%) 85.7%, 140 | hsl(222.86, 100%, 32.94%) 100% 141 | ); 142 | } 143 | .color-option > div.red { 144 | background: linear-gradient( 145 | to bottom right, 146 | hsl(0, 0%, 100%) 0%, 147 | hsl(0, 54.9%, 99.24%) 0.3%, 148 | hsl(0, 54.9%, 97.08%) 1.4%, 149 | hsl(0, 54.9%, 93.76%) 3.2%, 150 | hsl(0, 54.9%, 89.48%) 5.8%, 151 | hsl(0, 54.9%, 84.44%) 9.3%, 152 | hsl(0, 54.9%, 78.88%) 13.6%, 153 | hsl(0, 54.9%, 73%) 18.9%, 154 | hsl(0, 54.9%, 67%) 25.1%, 155 | hsl(0, 54.9%, 61.12%) 32.4%, 156 | hsl(0, 54.9%, 55.56%) 40.7%, 157 | hsl(0, 54.9%, 50.52%) 50.2%, 158 | hsl(0, 63.83%, 46.24%) 60.8%, 159 | hsl(0, 73.03%, 42.92%) 72.6%, 160 | hsl(0, 79.78%, 40.76%) 85.7%, 161 | hsl(0, 82.35%, 40%) 100% 162 | ); 163 | } 164 | 165 | @media(min-width: 576px) { 166 | .color-option { 167 | width: 65px; 168 | height: 65px; 169 | } 170 | .image-option { 171 | width: 100px; 172 | height: 100px; 173 | } 174 | } 175 | @media(min-width: 992px) { 176 | .text-option { 177 | max-width: 300px; 178 | } 179 | } -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | export const colors = [ 2 | { label: "Pearl White Multi-Coat", value: "white", price: 0 }, 3 | { label: "Solid Black", value: "black", price: 1500 }, 4 | { label: "Midnight Silver Metallic", value: "silver", price: 1500 }, 5 | { label: "Deep Blue Metallic", value: "blue", price: 1500 }, 6 | { label: "Red Multi-Coat", value: "red", price: 2500 } 7 | ]; 8 | 9 | export const interiorColors = [ 10 | { label: "All black Figured Ash Wood Décor", value: "all_black", price: 0 }, 11 | { label: "Black and white Dark Ash Wood Décor", value: "black_and_white", price: 1500 }, 12 | { label: "Cream Oak Wood Décor", value: "cream", price: 1500 }, 13 | ]; 14 | 15 | export const interiorLayouts = [ 16 | { label: "Five seat interior", value: "five_seat", price: 0 }, 17 | { label: "Six seat interior", value: "six_seat", price: 6500 }, 18 | { label: "Seven seat interior", value: "seven_seat", price: 3500 }, 19 | ]; 20 | 21 | export const models = [ 22 | { 23 | key: 's', 24 | name: "Model S", 25 | colors: colors, 26 | wheels: [ 27 | { 28 | src: `${process.env.PUBLIC_URL}/wheels/model_s/model_s_wheel_1.png`, 29 | label: '19" Tempest Wheels', 30 | value: "wheel_1", 31 | price: 0 32 | }, 33 | { 34 | src: `${process.env.PUBLIC_URL}/wheels/model_s/model_s_wheel_2.png`, 35 | label: '21" Sonic Carbon Twin Turbine Wheels', 36 | value: "wheel_2", 37 | price: 4500 38 | } 39 | ], 40 | types: [ 41 | { 42 | label: "Long Range Plus", 43 | value: "long_range_plus", 44 | specs: { 45 | range: 402, 46 | top_speed: 155, 47 | acceleration_time: 3.7, 48 | }, 49 | price: 69420 50 | }, 51 | { 52 | label: "Performance", 53 | value: "performance", 54 | specs: { 55 | range: 387, 56 | top_speed: 163, 57 | acceleration_time: 2.3, 58 | }, 59 | price: 91990, 60 | benefits: [ 61 | "Quicker acceleration: 0-60 mph in 2.3s", 62 | "Ludicrous Mode", 63 | "Enhanced Interior Styling", 64 | "Carbon fiber spoiler" 65 | ] 66 | }, 67 | { 68 | label: "Plaid", 69 | value: "plaid", 70 | specs: { 71 | range: 520, 72 | top_speed: 200, 73 | acceleration_time: 2.0, 74 | }, 75 | price: 139990, 76 | benefits: [ 77 | "Quickest 0-60 mph and quarter mile acceleration of any production car ever", 78 | "Acceleration from 0-60 mph: <2.0s", 79 | "Quarter mile: <9.0s", 80 | "1,100+ horsepower", 81 | "Tri Motor All-Wheel Drive" 82 | ] 83 | }, 84 | ], 85 | interiorColors: interiorColors 86 | }, 87 | { 88 | key: 'x', 89 | name: "Model X", 90 | colors: colors, 91 | wheels: [ 92 | { 93 | src: `${process.env.PUBLIC_URL}/wheels/model_x/model_x_wheel_1.png`, 94 | label: '20" Silver Wheels', 95 | value: "wheel_1", 96 | price: 0 97 | }, 98 | { 99 | src: `${process.env.PUBLIC_URL}/wheels/model_x/model_x_wheel_2.png`, 100 | label: '22" Onyx Black Wheels', 101 | value: "wheel_2", 102 | price: 5500 103 | } 104 | ], 105 | types: [ 106 | { 107 | label: "Long Range Plus", 108 | value: "long_range_plus", 109 | specs: { 110 | range: 371, 111 | top_speed: 155, 112 | acceleration_time: 4.4 113 | }, 114 | price: 79900 115 | }, 116 | { 117 | label: "Performance", 118 | value: "performance", 119 | specs: { 120 | range: 341, 121 | top_speed: 163, 122 | acceleration_time: 2.6 123 | }, 124 | price: 99990, 125 | benefits: [ 126 | "Quicker acceleration: 0-60 mph in 2.6s", 127 | "Ludicrous Mode", 128 | "Enhanced Interior Styling" 129 | ] 130 | } 131 | ], 132 | interiorColors: interiorColors, 133 | interiorLayouts: interiorLayouts 134 | }, 135 | { 136 | key: 'y', 137 | name: "Model Y", 138 | colors: colors, 139 | wheels: [ 140 | { 141 | src: `${process.env.PUBLIC_URL}/wheels/model_y/model_y_wheel_1.png`, 142 | label: '19’’ Gemini Wheels', 143 | value: "wheel_1", 144 | price: 0 145 | }, 146 | { 147 | src: `${process.env.PUBLIC_URL}/wheels/model_y/model_y_wheel_2.png`, 148 | label: '20’’ Induction Wheels', 149 | value: "wheel_2", 150 | price: 2000 151 | } 152 | ], 153 | types: [ 154 | { 155 | label: "Long Range", 156 | value: "long_range", 157 | specs: { 158 | range: 326, 159 | top_speed: 135, 160 | acceleration_time: 4.8 161 | }, 162 | price: 45690 163 | }, 164 | { 165 | label: "Performance", 166 | value: "performance", 167 | specs: { 168 | range: 303, 169 | top_speed: 155, 170 | acceleration_time: 3.5 171 | }, 172 | price: 55690, 173 | benefits: [ 174 | "Increased top speed from 135mph to 155mph", 175 | "21’’ Überturbine Wheels", 176 | "Performance Brakes", 177 | "Lowered suspension", 178 | "Aluminum alloy pedals" 179 | ] 180 | } 181 | ], 182 | interiorColors: interiorColors.slice(0,2), 183 | interiorLayouts: [interiorLayouts[0], interiorLayouts[2]] 184 | } 185 | ]; 186 | 187 | export const initialConfig = { 188 | 's': { 189 | car_type: "long_range_plus", 190 | model: "s", 191 | color: "white", 192 | wheels: "wheel_1", 193 | interior_color: "all_black" 194 | }, 195 | 'x': { 196 | car_type: "long_range_plus", 197 | model: "x", 198 | color: "white", 199 | wheels: "wheel_1", 200 | interior_color: "all_black", 201 | interior_layout: "five_seat" 202 | }, 203 | 'y': { 204 | car_type: "long_range", 205 | model: "y", 206 | color: "white", 207 | wheels: "wheel_1", 208 | interior_color: "all_black", 209 | interior_layout: "five_seat" 210 | } 211 | }; -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 16 | 20 | 28 | 32 | 38 | 44 | 49 | 51 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /public/logo_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 16 | 20 | 28 | 32 | 38 | 44 | 49 | 51 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { models, initialConfig } from '../../data'; 3 | // Styles 4 | import './App.css'; 5 | // Components 6 | import Menu from '../Menu'; 7 | import Footer from '../Footer'; 8 | import Settings from '../Settings'; 9 | import Summary from '../Summary'; 10 | import Preview from '../Preview'; 11 | import InteriorPreview from '../InteriorPreview'; 12 | 13 | /* 14 | * TODO: Refactor App as a functional component 15 | * 16 | * Requirements: 17 | * - Compute total price using React hooks only when config or selectedModel change 18 | * - Create a custom hook to use localStorage to store the current step and config 19 | * - Switch from setState to the useLocalStorage hook 20 | * - Use function closures instead of this for callbacks and event handlers 21 | * - App logic and behavior should remain the same 22 | * 23 | */ 24 | class App extends React.Component { 25 | state = { 26 | currentStep: 0, 27 | config: initialConfig?.['s'] ?? null 28 | }; 29 | 30 | get selectedModel() { 31 | return models.find(model => 32 | model?.key === this.state.config?.model 33 | ); 34 | }; 35 | 36 | get steps() { 37 | return [ 38 | { 39 | name: "car", 40 | settings: [ 41 | { 42 | label: "Select car", 43 | type: "text", 44 | prop: "model", 45 | options: models.map(model => ({ 46 | value: model.key, 47 | label: model.name 48 | })) 49 | }, 50 | { 51 | label: "Select type", 52 | type: "text", 53 | prop: "car_type", 54 | options: this.selectedModel?.types ?? [], 55 | disclaimer_1: "All cars have Dual Motor All-Wheel Drive, adaptive air suspension, premium interior and sound.", 56 | disclaimer_2: "Tesla All-Wheel Drive has two independent motors that digitally control torque to the front and rear wheels—for far better handling and traction control. Your car can drive on either motor, so you don't need to worry about getting stuck on the road." 57 | } 58 | ] 59 | }, 60 | { 61 | name: "exterior", 62 | settings: [ 63 | { 64 | label: "Select color", 65 | type: "color", 66 | prop: "color", 67 | options: this.selectedModel?.colors ?? [] 68 | }, 69 | { 70 | label: "Select wheels", 71 | type: "image", 72 | prop: "wheels", 73 | options: this.selectedModel?.wheels ?? [] 74 | } 75 | ] 76 | }, 77 | { 78 | name: "interior", 79 | settings: [ 80 | { 81 | label: "Select premium interior", 82 | type: "text", 83 | prop: "interior_color", 84 | options: this.selectedModel?.interiorColors ?? [] 85 | }, 86 | { 87 | label: "Select interior layout", 88 | type: "text", 89 | prop: "interior_layout", 90 | options: this.selectedModel?.interiorLayouts ?? [] 91 | }, 92 | ] 93 | }, 94 | { 95 | name: "summary" 96 | } 97 | ]; 98 | }; 99 | 100 | get totalPrice() { 101 | const basePrice = this.selectedModel?.types?.find( 102 | type => type.value === this.state.config?.car_type 103 | )?.price ?? 0; 104 | const colorPrice = this.selectedModel?.colors?.find( 105 | color => color.value === this.state.config?.color 106 | )?.price ?? 0; 107 | const wheelsPrice = this.selectedModel?.wheels?.find( 108 | wheels => wheels.value === this.state.config?.wheels 109 | )?.price ?? 0; 110 | const interiorColorPrice = this.selectedModel?.interiorColors?.find( 111 | interiorColor => interiorColor.value === this.state.config?.interior_color 112 | )?.price ?? 0; 113 | const interiorLayoutPrice = this.selectedModel?.interiorLayouts?.find( 114 | interiorLayout => interiorLayout.value === this.state.config?.interior_layout 115 | )?.price ?? 0; 116 | 117 | return basePrice + colorPrice + wheelsPrice + interiorColorPrice + interiorLayoutPrice; 118 | }; 119 | 120 | goToStep = (step) => { 121 | this.setState({ currentStep: step }); 122 | }; 123 | 124 | goToPrevStep = () => { 125 | this.setState(prevState => { 126 | const newStep = prevState.currentStep > 0 127 | ? prevState.currentStep-1 128 | : prevState.currentStep; 129 | return { currentStep: newStep }; 130 | }); 131 | }; 132 | 133 | goToNextStep = () => { 134 | this.setState(prevState => { 135 | const newStep = prevState.currentStep < this.steps.length - 1 136 | ? prevState.currentStep+1 137 | : prevState.currentStep; 138 | return { currentStep: newStep }; 139 | }); 140 | }; 141 | 142 | handleChangeModel = (model) => { 143 | this.setState({ config: initialConfig[model] }); 144 | }; 145 | 146 | handleOnSelectOption = (prop, value) => { 147 | if (prop === "model") { 148 | this.handleChangeModel(value); 149 | } 150 | else { 151 | this.setState(prevState => ({ 152 | config: { 153 | ...prevState.config, 154 | [prop]: value 155 | } 156 | })); 157 | } 158 | }; 159 | 160 | render() { 161 | const isFirstStep = this.state.currentStep === 0; 162 | const isLastStep = this.state.currentStep === this.steps.length - 1; 163 | 164 | return ( 165 |
166 | step.name)} 168 | selectedItem={this.state.currentStep} 169 | onSelectItem={this.goToStep} 170 | /> 171 |
172 | { 173 | this.steps[this.state.currentStep]?.name === "interior" ? ( 174 | interiorColor.value === this.state.config.interior_color 177 | )} 178 | /> 179 | ) : ( 180 | 187 | ) 188 | } 189 | { 190 | isLastStep ? ( 191 | 196 | ) : ( 197 | 202 | ) 203 | } 204 |
205 |
213 | ); 214 | }; 215 | }; 216 | 217 | export default App; 218 | --------------------------------------------------------------------------------