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

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
;
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 |
41 |
42 |
43 | {this.props.items.map((item, i) => (
44 | - this.props.onSelectItem(i)}
47 | className={this.props.selectedItem === i ? 'selected' : null}
48 | >
49 |
{item}
50 |
51 | ))}
52 |
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 | [](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 | 
33 |
34 | 5. Open [http://localhost:3000](http://localhost:3000) to view your React application in the browser
35 |
36 | 
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 |
77 |
--------------------------------------------------------------------------------
/public/logo_white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
213 | );
214 | };
215 | };
216 |
217 | export default App;
218 |
--------------------------------------------------------------------------------