├── .gitignore ├── README.md ├── data └── data.json ├── source ├── .env.example ├── .gitignore ├── package.json ├── public │ └── index.html └── src │ ├── App.js │ ├── components │ ├── header │ │ └── Header.js │ ├── home │ │ └── Home.js │ ├── login │ │ ├── Login.js │ │ └── LoginForm.js │ ├── nav │ │ └── Nav.js │ └── row │ │ └── Row.js │ ├── firebase │ └── firebase.js │ ├── index.css │ └── index.js └── ui ├── index.html ├── login.html └── styles.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock 3 | **/.env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Netflix Clone 2 | 3 | ![](./images/1.png) 4 | 5 | ## **Preface** 6 | 7 | Here are some concepts that this repository will cover: 8 | 9 | - JSX. 10 | - React elements 11 | - React components 12 | - Firebase. 13 | - useEffect. 14 | - state. 15 | - useState. 16 | - props. 17 | - React Router 18 | - Event handling. 19 | - Form handling. 20 | - Synthetic events 21 | - Communicate between components 22 | - Deploy React to Firebase. 23 | 24 | > Agenda: 25 | > 26 | > - Episode 1: Introduce the course, common rendering types, and import sample data to Firebase and understand React elements, React components by building Netflix's header component. 27 | > 28 | > - Episode 2: Create Row component, get data from Firebase, and understand about useEffect, props, state, useState by refactoring the Row component. 29 | > 30 | > - Episode 3: React Router to navigate between pages and create a "Sign In" page. 31 | > 32 | > - Episode 4: Summary knowledge and deploy the project to Firebase. 33 | 34 | ## **Table of Contents** 35 | 36 | | No. | Topics | 37 | | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 38 | | 0 | [How to Run the Project.](#how-to-run-the-project) | 39 | | 1 | [Live Demo.](#live-demo) | 40 | | 2 | [Prequisites.](#prequisites) | 41 | | 2.1 | [      Softwares.](#softwares) | 42 | | 2.2 | [      Technical Skills.](#technical-skills) | 43 | | 2.3 | [      Materials.](#materials) | 44 | | 3 | [Purposes of the Course.](#purposes-of-the-course) | 45 | | 3.1 | [      Final Project.](#final-project) | 46 | | 3.2 | [      Job.](#job) | 47 | | 4 | [Common Rendering Types.](#common-rendering-types) | 48 | | 4.1 | [      Server Side Rendring.](#server-side-rendering) | 49 | | 4.1.1 | [            How Server Side Rendering Work.](#how-server-side-rendering-work) | 50 | | 4.1.2 | [            The Advantages of Server Side Rendering.](#the-advantages-of-server-side-rendering) | 51 | | 4.1.3 | [            The Disadvantages of Server Side Rendering.](#the-disadvantages-of-server-side-rendering) | 52 | | 4.2 | [      Client Side Rendring & Single Page Application.](#client-side-rendering-and-single-page-application) | 53 | | 4.2.1 | [            How Client Side Rendering Work.](#how-client-side-rendering-work) | 54 | | 4.2.2 | [            The Advantages of Client Side Rendering.](#the-advantages-of-client-side-rendering) | 55 | | 4.2.3 | [            The Disadvantages of Client Side Rendering.](#the-disadvantages-of-client-side-rendering) | 56 | | 5 | [Introduction about React.](#introduction-about-react) | 57 | | 5.1 | [      What.](#what) | 58 | | 5.2 | [      Why & Advantages.](#why-and-advantages) | 59 | | 5.3 | [      When & Disadvantages.](#when-and-disadvantages) | 60 | | 5.4 | [      How.](#how) | 61 | | 5.5 | [      What Makes React so Fast.](#what-makes-react-so-fast) | 62 | | 6 | [Building Netflix.](#building-netflix) | 63 | | 6.1 | [      How to Import Sample Data to Firebase.](#how-to-import-sample-data-to-firebase) | 64 | | 6.2 | [      Create Web Application on Firebase.](#create-web-application-on-firebase) | 65 | | 6.3 | [      Prequesites.](#netflix-prequesites) | 66 | | 6.4 | [      Build Header - Home Page.](#build-header-home-page) | 67 | | 6.5 | [      Build Row - Home Page.](#build-row-home-page) | 68 | | 6.6 | [      Apply React Router.](#apply-react-router) | 69 | | 6.7 | [      Set Up Firebase Authentication.](#set-up-firebase-authentication) | 70 | | 6.8 | [      Build Netflix Login Page.](#build-netflix-login-page) | 71 | | 7 | [Deploy React to Firebase.](#deploy-react-to-firebase) | 72 | | 8 | [Summary.](#summary) | 73 | | 9 | [Useful Resources.](#useful-resources) | 74 | | 10 | [References.](#references) | 75 | 76 | 77 | 78 | ## **0. How to Run the Project.** 79 | 80 | - Step 1: Clone the project by using git clone or download the zip file. 81 | 82 | - Step 2: Open "terminal" / "cmd" / "gitbash" and change directory to "source" and run "npm install" to install dependencies. 83 | 84 | - Step 3: Run "npm start" to run the fron-end project. 85 | 86 | 87 | 88 | ## **1. Live Demo.** 89 | 90 | // TODO 91 | 92 | 93 | 94 | ## **2. Prequisites.** 95 | 96 | 97 | 98 | ### **2.1. Softwares.** 99 | 100 | - Install NodeJS. 101 | 102 | - An IDE or a text editor (VSCode, Intellij, Webstorm, etc). 103 | 104 | 105 | 106 | ### **2.2. Technical Skills.** 107 | 108 | - Basic programming skill. 109 | 110 | - Basic HTML, CSS, JS skills. 111 | 112 | 113 | 114 | ### **2.3. Materials.** 115 | 116 | - Html, CSS, js (source code) was prepared because I want to focus on React and share knowledge about React. Building HTML and CSS from scratch would take a lot of time. 117 | 118 | - README.md (the MD file will contain everything about the course). 119 | 120 | - Netflix data will be used to import to Firebase. In this course, we use Firebase as our back-end service. 121 | 122 | 123 | 124 | ## **3. Purposes of the Course.** 125 | 126 | 127 | 128 | ### **3.1. Final Project.** 129 | 130 | - The course would help you have an understanding of React. 131 | 132 | - You could build the final project with an end-to-end solution (front-end solution using React and back-end solution using Firebase). 133 | 134 | 135 | 136 | ### **3.2. Job.** 137 | 138 | - After finishing the course, you could get a job in a fresher / junior position. 139 | 140 | 141 | 142 | ## **4. Common Renderting Types.** 143 | 144 | 145 | 146 | ### **4.1 Server Side Rendering.** 147 | 148 | 149 | 150 | #### **4.1.1. How Server Side Rendering Work.** 151 | 152 | ![](./images/2.png) 153 | 154 | - When a user accesses the website. The request would be sent to the server. 155 | 156 | - The web server would receive the request and connect to the database. 157 | 158 | - The web server would render HTML and return the result to the end-user. 159 | 160 | 161 | 162 | #### **4.1.2 The Advantages of Server Side Rendering.** 163 | 164 | - The initial loading time will be fast. Because almost all of the things will be handled on the server-side. 165 | 166 | - It is supported by many frameworks (Node.js, Java, PHP, .NET, etc). 167 | 168 | - It is easy to understand because the developers do not need to separate front-end and back-end. 169 | - It is good for SEO. 170 | 171 | 172 | 173 | #### **4.1.3 The Disadvantages of Server Side Rendering.** 174 | 175 | - When the user navigates between pages. The entire page has to be loaded again. 176 | 177 | - The server side has to handle many things (connecting to the database, rendering HTML, etc). 178 | 179 | - It consumes more bandwidth because the server returns the same content many times (header, footer, etc). 180 | 181 | 182 | 183 | ### **4.2 Client Side Rendering & Single Page Application.** 184 | 185 | 186 | 187 | #### **4.2.1. How Client Side Rendering Work.** 188 | 189 | ![](./images/3.png) 190 | 191 | - Rendering HTML, CSS will be handled on the browser by Javascript. 192 | 193 | 194 | 195 | #### **4.2.2. The Advantages of Client Side Rendering.** 196 | 197 | - The page would be loaded just once. 198 | 199 | - Javascript code would interact with the back-end APIs to get data. 200 | 201 | - Business logic could be handled on the client side. Hence, the server does not need to handle all of the business features. 202 | 203 | - Reducing the bandwidth because the applications just need to get JSON data instead of loading the entire page. 204 | 205 | - It provides better UX. 206 | 207 | 208 | 209 | #### **4.2.3. The Disadvantages of Client Side Rendering.** 210 | 211 | - The initial loading time will be slow. Because the browser has to load Javascript code, get data from the server, parse and render data. 212 | 213 | - The project will be separated into front-end and back-end. 214 | 215 | - The application could not be run if Javascript is disabled on the browser. 216 | 217 | - It is not good for SEO. 218 | 219 | - The load time would be slow for old devices. 220 | 221 | 222 | 223 | ## **5. Introduction about React.** 224 | 225 | 226 | 227 | ### **5.1. What.** 228 | 229 | - React is a front-end open source library. It will be used to build SPA (Single Page Application). 230 | 231 | - React Native can be used to build mobile applications which have the ability to run on both IOS and Android. 232 | 233 | - React was created by Jordan Walke - a software engineer for Facebook. 234 | 235 | - React was first deployed on Facebook's New Feeds in 2011 and on Instagram in 2012. 236 | 237 | 238 | 239 | ### **5.2 Why & Advantages.** 240 | 241 | - React has a Virtual DOM concept so that it provides good performance. 242 | 243 | - React supports server-side rendering. 244 | 245 | - React supports one-way data flow. 246 | 247 | - React helps the developer create reusable components. 248 | 249 | 250 | 251 | ### **5.3. When & Disadvantages.** 252 | 253 | - React is just a view library, not a full framework. 254 | 255 | - There is a curve for beginners. 256 | 257 | - Integrating React with MVC traditional frameworks will need 258 | some configuration. 259 | 260 | - The code complexity will be increased. 261 | 262 | - Your application may contain too many smaller components. 263 | 264 | 265 | 266 | ### **5.4. How.** 267 | 268 | - Step 1: Install Node.js. 269 | 270 | - Step 2: Run 'npx create-react-app appName'. (appName could be replaced by the application's name). 271 | 272 | 273 | 274 | ### **5.5. What Makes React so Fast.** 275 | 276 | - The answer is Virtual DOM. 277 | 278 | - There is a process which is called reconciliation. 279 | 280 | 1. Whenever any underlying data changes, the entire UI is re-rendered in Virtual DOM representation. 281 | 282 | ![](./images/4.png) 283 | 284 | 2. The differences between the Virtual DOM and the Real DOM are calculated. 285 | 286 | ![](./images/5.png) 287 | 288 | 3. The Real DOM will be updated with only the things that have actually changed. 289 | 290 | ![](./images/6.png) 291 | 292 | 293 | 294 | ## **6. Building Netflix.** 295 | 296 | 297 | 298 | ### **6.1. How to Import Sample Data to Firebase.** 299 | 300 | - Step 1: Access the browser, go to https://firebase.google.com and click on the "Sign in" button. 301 | 302 | ![](./images/7.png) 303 | 304 | - Step 2: Sign in to Firebase by using a Gmail account, input the user's name and password and then click on the "Next" button. 305 | 306 | ![](./images/8.png) 307 | 308 | - Step 3: Click on "Go to Console" button. 309 | 310 | ![](./images/9.png) 311 | 312 | - Step 4: Click on the "Create a project" button to create the Firebase project. 313 | 314 | ![](./images/10.png) 315 | 316 | - Step 5: Input project's name (example: 'netflix-clone') and then click on "Continue" button. 317 | 318 | ![](./images/11.png) 319 | 320 | - Step 6: Click on the "Continute" button. 321 | 322 | ![](./images/12.png) 323 | 324 | - Step 7: Select the account. You could select your Gmail account and then click on the "Create Project" button. 325 | 326 | ![](./images/13.png) 327 | 328 | - Step 8: In this step, Firebase would handle the remaining tasks for you and you wait until everything has been set up successfully. 329 | 330 | ![](./images/14.png) 331 | 332 | - Step 9: Click on "Continue" button. 333 | 334 | ![](./images/15.png) 335 | 336 | - Step 10: At the dashboard page, you click the "Realtime Database" option. It means that when you change your data, your data on the web application will be updated automatically without refreshing the page. 337 | 338 | ![](./images/16.png) 339 | 340 | - Step 11: It's time to create a database by clicking on the "Create Database" button. 341 | 342 | ![](./images/17.png) 343 | 344 | - Step 12: Select the real-time database's location (just need to choose the default one) and then click on the "Next" button. 345 | 346 | ![](./images/18.png) 347 | 348 | - Step 13: Configure security rules for the database. You select test mode because the database is used for demo purposes and click on the "Enable" button. 349 | 350 | ![](./images/19.png) 351 | 352 | - Step 14: Click on the "Import JSON" option. 353 | 354 | ![](./images/20.png) 355 | 356 | - Step 15: Select "data.json" from the git repository and click on the "Import" button. 357 | 358 | ![](./images/21.png) 359 | 360 | - Step 16: After importing successfully, your result should be like this. 361 | 362 | ![](./images/22.png) 363 | 364 | 365 | 366 | ### **6.2. Create Web Application on Firebase.** 367 | 368 | After creating the real-time database and importing sample data to Firebase, a web application should be created on Firebase. Firebase would return the configuration information. That information will be used later. 369 | 370 | - Step 1: Click on the "setting" icon and choose the "Project settings" option. 371 | 372 | ![](./images/23.png) 373 | 374 | - Step 2: Scroll down to the bottom of the page and choose the "web" icon. 375 | 376 | ![](./images/24.png) 377 | 378 | - Step 3: Input "App nickname" (example: "netflix-clone") and click on "Register App" button. 379 | 380 | ![](./images/25.png) 381 | 382 | - Step 4: Save Firebase configuration somewhere else for later use and click on the "Continue to Console" button. 383 | 384 | ![](./images/26.png) 385 | 386 | 387 | 388 | ### **6.3. Prequesites.** 389 | 390 | - Step 1: Imported data.json (in the data folder) to Firebase. We will use that data to build a Netflix application. 391 | 392 | - Step 2: Created the web application on Firebase to get Firebase configuration. 393 | 394 | - Step 3: Created a project with name 'netflix-clone' by running 'npx create-react-app netflix-clone'. 395 | 396 | - Step 4: Replaced the content of styles.css (in the ui folder) to index.css so that we do not need to worry about CSS. 397 | 398 | 399 | 400 | ### **6.4. Build Header - Home Page.** 401 | 402 | ![](./images/27.png) 403 | 404 | The following steps describe how to build Netflix's header. 405 | 406 | - Step 1: Replace the content of App.js with the following code, open "cmd" or "terminal" or "git-bash", change the directory to your project's folder and run "npm start" to launch the project. 407 | 408 | ```js 409 | function App() { 410 | return ( 411 |
412 | {/* Nav */} 413 |
414 | 419 | 424 |
425 | {/* End Nav */} 426 | {/* Header */} 427 |
428 |
429 |

Ginny & Georgia

430 |
431 | 432 | 433 |
434 |

435 | Angsty and awkward fifteen year old Ginny Miller often feels more 436 | mature than her thirty year old mother, the irresistible and dynamic 437 | Georgia Miller... 438 |

439 |
440 |
441 |
442 | {/* Header */} 443 |
444 | ); 445 | } 446 | 447 | export default App; 448 | ``` 449 | 450 | > 1st NOTE: 451 | > 452 | > - What inside return () is called React Element. 453 | > 454 | > - React Element describes what would be appeared on the screen. 455 | > 456 | > - React Element is cheap. 457 | > 458 | > - Once React Element is created, they never are mutated. 459 | > 460 | > - As mentioned above, we are writing Javascript along with HTML, we can do that because React provides the concept of JSX (Javascript XML). 461 | 462 | > 2nd NOTE: 463 | > 464 | > - If you run the code, Netflix's header would have appeared. 465 | > 466 | > - However, we can break down Netflix's header into smaller components to reuse them and it would be easy to maintain. 467 | > 468 | > - Components are small UI pieces that will be combined to build your application. 469 | > 470 | > - You can imagine you are playing Lego. 471 | 472 | ![](./images/28.png) 473 | 474 | - Step 2: Create a "components" folder." components" folder will be used to store components in your application. We should not put everything in the src folder, structuring projects helps the developers scale and maintain code easier. 475 | 476 | - Step 3: Create Nav.js file in "components/nav" folder with the following code. 477 | 478 | ```js 479 | function Nav() { 480 | return ( 481 |
482 | 487 | 492 |
493 | ); 494 | } 495 | 496 | export default Nav; 497 | ``` 498 | 499 | - Step 4: Create Header.js file in "components/header" folder with the following code. 500 | 501 | ```js 502 | function Header() { 503 | return ( 504 |
505 |
506 |

Ginny & Georgia

507 |
508 | 509 | 510 |
511 |

512 | Angsty and awkward fifteen year old Ginny Miller often feels more 513 | mature than her thirty year old mother, the irresistible and dynamic 514 | Georgia Miller... 515 |

516 |
517 |
518 |
519 | ); 520 | } 521 | 522 | export default Header; 523 | ``` 524 | 525 | - Step 5: Change your App.js with the following code. 526 | 527 | ```js 528 | import Nav from "./components/nav/Nav"; 529 | import Header from "./components/header/Header"; 530 | 531 | function App() { 532 | return ( 533 |
534 | {/* Nav */} 535 |
541 | ); 542 | } 543 | 544 | export default App; 545 | ``` 546 | 547 | > 3rd NOTE: 548 | > 549 | > - Now you can see the result would be the same. 550 | > 551 | > - You can reuse the Nav component and Header component in different places in your application. 552 | > 553 | > - It is one of the advantages of React library - create reusable components. 554 | 555 | 556 | 557 | ### **6.5. Build Row - Home Page** 558 | 559 | ![](./images/29.png) 560 | 561 | The above image mentions the similarities between rows in the home page. They also have a title element and a list of movies element. Therefore, a "Row" component should be created for reusable. 562 | 563 | The following steps describe how to build the "Row" component. 564 | 565 | - Step 1: Like what we have done with the Nav component and Header component, you create a Row.js file in the "components/row folder with the following code. 566 | 567 | ```js 568 | function Row() { 569 | return ( 570 |
571 |

NETFLIX ORGINALS

572 |
573 | Jupiter's Legacy 578 | Lucifer 583 | Luis Miguel: The Series 588 | Selena: The Series 593 | Who Killed Sara? 598 | Love, Death & Robots 603 | Cobra Kai 608 | Elite 613 | Stranger Things 618 | Money Heist 623 | The Umbrella Academy 628 | Haunted: Latin America 633 | Lupin 638 | Chilling Adventures of Sabrina 643 | Castlevania 648 | Ginny & Georgia 653 | Sex Education 658 | Halston 663 | Dark 668 | The Innocent 673 |
674 |
675 | ); 676 | } 677 | 678 | export default Row; 679 | ``` 680 | 681 | > 4th NOTE: 682 | > 683 | > - In case, we have to create different rows with different data (images, title, and so on), we have to duplicate this component to many "Row" components. 684 | > 685 | > - As mentioned before, **the component should be reusable**. It means that we need to make the current "Row" component be reusable instead of creating a new one. 686 | 687 | > 5th NOTE: 688 | > 689 | > - We are hardcoding the data in the "Row" component. 690 | > 691 | > - We need to get some real data to make the application be realistic. 692 | > 693 | > - In this course, we need to know how to integrate with Firebase first, and then we will come back to our "Row" component later. 694 | 695 | - Step 4: Install Firebase package from npm by running 'npm install --save firebase". 696 | 697 | - Step 5: Create "firebase" inside the src folder. 698 | 699 | - Step 6: Create a "firebase.js" file inside the "firebase" folder with the following content. 700 | 701 | ```js 702 | import firebase from "firebase"; 703 | 704 | const app = firebase.initializeApp({ 705 | apiKey: "apiKey", 706 | authDomain: "projectId.firebaseapp.com", 707 | // For databases not in the us-central1 location, databaseURL will be of the 708 | // form https://[databaseName].[region].firebasedatabase.app. 709 | // For example, https://your-database-123.europe-west1.firebasedatabase.app 710 | databaseURL: "https://databaseName.firebaseio.com", 711 | storageBucket: "bucket.appspot.com", 712 | }); 713 | 714 | const firebaseDatabase = app.database(); 715 | 716 | export { firebaseDatabase }; 717 | ``` 718 | 719 | > 6th NOTE: 720 | > 721 | > - Replace "apiKey" with your API key. 722 | > 723 | > - Replace "projectId.firebaseapp.com" with your auth domain. 724 | > 725 | > - Replace "https://databaseName.firebaseio.com" with your database URL. 726 | > 727 | > - Replace "bucket.appspot.com" with your storage bucket. 728 | 729 | - Step 7: In fact, we should store credentials in a .env file (environment file). Env file should not be committed to your git repository. Therefore, you need to create a .env file in your root folder with the following content. 730 | 731 | ```js 732 | REACT_APP_FIREBASE_API_KEY = xxx - xxx - xxx - xxx - xxx; 733 | REACT_APP_FIREBASE_AUTH_DOMAIN = xxx - xxx - xxx - xxx - xxx; 734 | REACT_APP_FIREBASE_DATABASE_UTL = xxx - xxx - xxx - xxx - xxx; 735 | REACT_APP_FIREBASE_STORAGE_BUCKET = xxx - xxx - xxx - xxx - xxx; 736 | ``` 737 | 738 | > 7th NOTE: 739 | > 740 | > - The prefix of your environment variables must be "REACT_APP". 741 | 742 | - Step 8: After creating the .env file, we need to replace the content of the firebase.js file with the following content. 743 | 744 | ```js 745 | import firebase from "firebase"; 746 | 747 | const app = firebase.initializeApp({ 748 | apiKey: `${process.env.REACT_APP_FIREBASE_API_KEY}`, 749 | authDomain: `${process.env.REACT_APP_FIREBASE_AUTH_DOMAIN}`, 750 | // For databases not in the us-central1 location, databaseURL will be of the 751 | // form https://[databaseName].[region].firebasedatabase.app. 752 | // For example, https://your-database-123.europe-west1.firebasedatabase.app 753 | databaseURL: `${process.env.REACT_APP_FIREBASE_DATABASE_UTL}`, 754 | storageBucket: `${process.env.REACT_APP_FIREBASE_STORAGE_BUCKET}`, 755 | }); 756 | 757 | const firebaseDatabase = app.database(); 758 | 759 | export { firebaseDatabase }; 760 | ``` 761 | 762 | - Step 9: Import "firebase database" from the "firebase.js" file and write a function in the "Row" component to get data from Firebase. 763 | 764 | ```js 765 | ... 766 | import { firebaseDatabase } from "../../firebase/firebase"; 767 | 768 | function Row() { 769 | ... 770 | const leafRoot = 'movies'; 771 | const fetchMovies = (movieType) => { 772 | const movieRef = firebaseDatabase.ref(`${leafRoot}/${movieType}`); 773 | movieRef.on("value", (snapshot) => { 774 | const movies = snapshot.val(); 775 | if (movies && movies.length !== 0) { 776 | setMovies(() => movies); 777 | } 778 | }); 779 | }; 780 | ... 781 | } 782 | ... 783 | ``` 784 | 785 | > 8th NOTE: 786 | > 787 | > - Where do we call the above function in the "Row" component? 788 | > 789 | > - The best practice is to call the function after the component has been loaded. How do we know when the component has been loaded? 790 | > 791 | > - React provides **"useEffect"** to help us. **"useEffect"** is one of the built-in React hooks. 792 | 793 | - Step 10: Import useEffect in the "Row" component. 794 | 795 | ```js 796 | import { useEffect } from "react"; 797 | ``` 798 | 799 | - Step 11: Call fetchMovies function in useEffect. 800 | 801 | ```js 802 | useEffect(() => { 803 | fetchMovies(movieType); 804 | }, []); 805 | ``` 806 | 807 | > 9th NOTE: 808 | > 809 | > - Wait a minute! What is "movieType" ??? We did not define it before. 810 | > 811 | > - Please do not worry, we will explain in detail why do we need "movieType" here. Firstly, please keep in mind that **we always want to make the "Row" component be reusable**. 812 | > 813 | > - For this reason, we should let the person who is using our "Row" component define the movie type that he/she wants to get from Firebase instead of hardcoding it in the "Row" component. 814 | > 815 | > - We want something like this. 816 | > 817 | > ```js 818 | > 819 | > 820 | > 821 | > 822 | > 823 | > 824 | > 825 | > ``` 826 | 827 | > 10th NOTE: 828 | > 829 | > - It is time for "props" to come to play. 830 | > 831 | > - "props" stands for properties. 832 | > 833 | > - "props" could be a single value or an object. 834 | > 835 | > - "props" would be passed from the parent component to the child component. 836 | > 837 | > - We can imagine the "props" object is similar to the function's parameters. 838 | 839 | - Step 12: Define props as the parameter for the Row function. 840 | 841 | ```js 842 | function Row(props) {...} 843 | ``` 844 | 845 | - Step 13: Get the value that will be passed from other components and use them in our application. 846 | 847 | ```js 848 | const { title, movieType } = props; 849 | ... 850 | useEffect(() => { 851 | fetchMovies(movieType); 852 | }, []); 853 | ... 854 |

{title}

855 | ``` 856 | 857 | - Step 14: Pass custom data from the "App" component to the "Row" component with the following code. 858 | 859 | ```js 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | ``` 869 | 870 | > 11th NOTE: 871 | > 872 | > - We are almost done! The question is how to display the list of movies on JSX after getting data from Firebase. 873 | > 874 | > - We need to find a way to store the data and make the component be re-rendered after we have movies from Firebase. 875 | > 876 | > - Fortunately, React provides **useState** to help us achieve that. 877 | 878 | > 12th NOTE: 879 | > 880 | > - State is an object that holds information that may be changed over the lifetime of the component. 881 | > 882 | > - It is private and fully controlled by the component. 883 | 884 | - Step 15: We need to import the "estate" in the "Row" component to define the movie's state. 885 | 886 | ```js 887 | import { useEffect, useState } from "react"; 888 | ``` 889 | 890 | - Step 16: Define the "movies" state to store the list of movies after getting data from Firebase. 891 | 892 | ```js 893 | function Row(props) { 894 | ... 895 | const [movies, setMovies] = useState([]); 896 | ... 897 | } 898 | ``` 899 | 900 | - Step 17: Update the "fetchMovies" function to put data to the state. 901 | 902 | ```js 903 | const fetchMovies = (movieType) => { 904 | const movieRef = firebaseDatabase.ref(`${leafRoot}/${movieType}`); 905 | movieRef.on("value", (snapshot) => { 906 | const movies = snapshot.val(); 907 | if (movies && movies.length !== 0) { 908 | // update "movies" state by calling "setMovies" function. 909 | setMovies(() => movies); 910 | } 911 | }); 912 | }; 913 | ``` 914 | 915 | > 13th NOTE: 916 | > 917 | > - In case, we have data in the "movies" state, the question is how to display the list of movies on JSX. Because we are displaying a long list of hardcoding image tags. 918 | > - We can use the map() function in Javascript to display the list of data on JSX. 919 | 920 | - Step 18: Display data in "movie" state on JSX by using map() function. 921 | 922 | ```js 923 | function Row(props) { 924 | ... 925 | return ( 926 |
927 |

{title}

928 |
929 | {movies.map((movie) => ( 930 | {movie.original_name} 935 | ))} 936 |
937 |
938 | ); 939 | ... 940 | } 941 | ``` 942 | 943 | - Step 19: Let's combine everything together. 944 | 945 | Row.js 946 | 947 | ```js 948 | import { useEffect, useState } from "react"; 949 | 950 | import { firebaseDatabase } from "../../firebase/firebase"; 951 | 952 | function Row(props) { 953 | const [movies, setMovies] = useState([]); 954 | 955 | const { title, movieType } = props; 956 | 957 | const leafRoot = "movies"; 958 | 959 | useEffect(() => { 960 | fetchMovies(movieType); 961 | }, []); 962 | 963 | const fetchMovies = (movieType) => { 964 | const movieRef = firebaseDatabase.ref(`${leafRoot}/${movieType}`); 965 | movieRef.on("value", (snapshot) => { 966 | const movies = snapshot.val(); 967 | if (movies && movies.length !== 0) { 968 | setMovies(() => movies); 969 | } 970 | }); 971 | }; 972 | 973 | return ( 974 |
975 |

{title}

976 |
977 | {movies.map((movie) => ( 978 | {movie.original_name} 983 | ))} 984 |
985 |
986 | ); 987 | } 988 | 989 | export default Row; 990 | ``` 991 | 992 | App.js 993 | 994 | ```js 995 | import Nav from "./components/nav/Nav"; 996 | import Header from "./components/header/Header"; 997 | import Row from "./components/row/Row"; 998 | 999 | function App() { 1000 | return ( 1001 |
1002 | {/* Nav */} 1003 |
1018 | ); 1019 | } 1020 | 1021 | export default App; 1022 | ``` 1023 | 1024 | 1025 | 1026 | ## **6.6. Apply React Router.** 1027 | 1028 | The Netflix application may contain many pages, not just the home page. Hence, we should find an efficient way to navigate between pages in our React application. **React Router** is a powerful routing library that can help us to achieve that. 1029 | 1030 | The following steps will demonstrate how to integrate React Router in our Netflix application and how we re-structure our components to support navigating between pages: 1031 | 1032 | - Step 1: Install "react-router-dom" library by running "npm install --save react-router-dom". 1033 | 1034 | > 14th NOTE: 1035 | > 1036 | > - We are writing code to build the home page in the App.js file. 1037 | > 1038 | > - In fact, we should not do that. We should create a "Home" component to store the code of the home page. 1039 | > 1040 | > - App.js should be used to store common information of the application such as routing, common components (navbar, header, footer, etc), and so on. 1041 | > 1042 | > - The diagram below demonstrates what our "Home" component looks like. 1043 | > 1044 | > ![](./images/30.png) 1045 | 1046 | - Step 2: Create a Home.js file in the "components" folder with the following code. 1047 | 1048 | ```js 1049 | import Nav from "../nav/Nav"; 1050 | import Header from "../header/Header"; 1051 | import Row from "../row/Row"; 1052 | 1053 | function Home() { 1054 | return ( 1055 |
1056 | {/* Nav */} 1057 |
1072 | ); 1073 | } 1074 | 1075 | export default Home; 1076 | ``` 1077 | 1078 | - Step 3: Update the App.js file with the following code. 1079 | 1080 | ```js 1081 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 1082 | 1083 | import Home from "./components/home/Home"; 1084 | 1085 | function App() { 1086 | return ( 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | ); 1095 | } 1096 | 1097 | export default App; 1098 | ``` 1099 | 1100 | > 15th NOTE: 1101 | > 1102 | > - If you run the application, the result will be the same. 1103 | > 1104 | > - As mentioned above, App.js should store common information of the application. In this case, it stores information about routing. 1105 | > 1106 | > - "Switch" component is similar to the switch-case in programming. According to the example above, the application will show the home page to the end-user if the current route is '/' 1107 | > 1108 | > - Following that, we can define many routes in the App.js file and specify the corresponding component for each route. We will do that in the following section. 1109 | 1110 | 1111 | 1112 | ## **6.7. Set Up Firebase Authentication.** 1113 | 1114 | Before building the login page, we need to set up Firebase Authentication first. Different sign-in methods could be integrated into the application. In this course, we will choose the "Email/Password" sign-in method. The following steps will describe how to set up that method in Firebase. 1115 | 1116 | - Step 1: Click on the "Authentication" option. 1117 | 1118 | ![](./images/31.png) 1119 | 1120 | - Step 2: Click on the "Get started" button. 1121 | 1122 | ![](./images/32.png) 1123 | 1124 | - Step 3: Choose the "Sign-in method" tab. 1125 | 1126 | ![](./images/33.png) 1127 | 1128 | - Step 4: Click on the "Edit" icon on the "Email/Password" row. 1129 | 1130 | ![](./images/34.png) 1131 | 1132 | - Step 5: Click on the "Enable" toggle button and click on the "Save" button. 1133 | 1134 | ![](./images/35.png) 1135 | 1136 | - Step 6: Set up an account for use later, choose the "Users" tab, and click on the "Add User" button. 1137 | 1138 | ![](./images/36.png) 1139 | 1140 | - Step 7: Input the user's name and password and click on the "Add User" button. 1141 | 1142 | ![](./images/37.png) 1143 | 1144 | - Step 8: After creating a new user successfully, your result should be like this. 1145 | 1146 | ![](./images/38.png) 1147 | 1148 | - Step 9: We need to update the "firebase.js" file for later use with the following code. 1149 | 1150 | ```js 1151 | import firebase from "firebase"; 1152 | 1153 | const app = firebase.initializeApp({ 1154 | apiKey: `${process.env.REACT_APP_FIREBASE_API_KEY}`, 1155 | authDomain: `${process.env.REACT_APP_FIREBASE_AUTH_DOMAIN}`, 1156 | // For databases not in the us-central1 location, databaseURL will be of the 1157 | // form https://[databaseName].[region].firebasedatabase.app. 1158 | // For example, https://your-database-123.europe-west1.firebasedatabase.app 1159 | databaseURL: `${process.env.REACT_APP_FIREBASE_DATABASE_UTL}`, 1160 | storageBucket: `${process.env.REACT_APP_FIREBASE_STORAGE_BUCKET}`, 1161 | }); 1162 | 1163 | const firebaseDatabase = app.database(); 1164 | const firebaseAuth = app.auth(); 1165 | 1166 | export { firebaseDatabase, firebaseAuth }; 1167 | ``` 1168 | 1169 | 1170 | 1171 | ## **6.8. Build Netflix Login Page.** 1172 | 1173 | ![](./images/39.png) 1174 | 1175 | The above image describes how the login page is separated into smaller components. In this case, we have a wrapper component which is called the "Login" component. On the other hand, the "Nav" component can be reused and the "Login Form" component should be created to create a sign-in form. The following steps will help us achieve that step by step. 1176 | 1177 | - Step 1: Create LoginForm.js in the "component/login" folder with the following code. 1178 | 1179 | ```js 1180 | function LoginForm() { 1181 | return ( 1182 |
1183 |
1184 |

Sign In

1185 |
1186 | 1187 |
1188 |
1189 | 1190 |
1191 | 1192 |
1193 | Remember me 1194 | Need help? 1195 |
1196 |
1197 |
1198 | fb 1202 | Login with Facebook 1203 |
1204 |
1205 | New to Netflix ? 1206 | Sign up now. 1207 |
1208 |
1209 | This page is protected by Google reCAPTCHA to ensure you're not a 1210 | bot. 1211 | Learn more. 1212 |
1213 |
1214 |
1215 |
1216 | ); 1217 | } 1218 | 1219 | export default LoginForm; 1220 | ``` 1221 | 1222 | - Step 2: Create Login.js file in "components/login" folder with the following code. 1223 | 1224 | ```js 1225 | import Nav from "../nav/Nav"; 1226 | import LoginForm from "./LoginForm"; 1227 | 1228 | function Login() { 1229 | return ( 1230 |
1231 | {/* Nav */} 1232 |
1238 | ); 1239 | } 1240 | 1241 | export default Login; 1242 | ``` 1243 | 1244 | > 16th NOTE: 1245 | > 1246 | > - The best part is about the "Nav" component can be reused without creating a new one. It is one of the advantages of React and component-based UI. 1247 | > 1248 | > - The login component was created. However, how the end-user can use it in the web application. We need to set up a new route in App.js so that the end-user can access that route and use the login page. 1249 | 1250 | - Step 3: Add "/login" route in App.js. App.js should look like this. 1251 | 1252 | ```js 1253 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 1254 | 1255 | import Home from "./components/home/Home"; 1256 | import Login from "./components/login/Login"; 1257 | 1258 | function App() { 1259 | return ( 1260 | 1261 | 1262 | 1263 | 1264 | 1265 | 1266 | 1267 | 1268 | 1269 | 1270 | ); 1271 | } 1272 | 1273 | export default App; 1274 | ``` 1275 | 1276 | > 17th NOTE: 1277 | > 1278 | > - If you run the project and access the '/login' route, you can see the login page. 1279 | > 1280 | > - We have finished dividing the login page into smaller components. The next part is how to detect when the current user clicks on the "Sign In" button. 1281 | 1282 | - Step 4: Replace the "Login Form" component with the following code. 1283 | 1284 | ```js 1285 | function LoginForm() { 1286 | const login = () => { 1287 | console.log("Sign in button was click"); 1288 | }; 1289 | 1290 | return ( 1291 |
1292 |
1293 |

Sign In

1294 |
1295 | 1296 |
1297 |
1298 | 1299 |
1300 | 1303 |
1304 | Remember me 1305 | Need help? 1306 |
1307 |
1308 |
1309 | fb 1313 | Login with Facebook 1314 |
1315 |
1316 | New to Netflix ? 1317 | Sign up now. 1318 |
1319 |
1320 | This page is protected by Google reCAPTCHA to ensure you're not a 1321 | bot. 1322 | Learn more. 1323 |
1324 |
1325 |
1326 |
1327 | ); 1328 | } 1329 | 1330 | export default LoginForm; 1331 | ``` 1332 | 1333 | > 18th NOTE: 1334 | > 1335 | > - In the code above, "onClick={login}" was added to the "Sign In" button, "login" function was created. It means that when the current user clicks on the "Sign In" button. the "login" function will be triggered and the message will be logged to the console. 1336 | > 1337 | > - The image below describes the result of the above code. 1338 | > 1339 | > ![](./images/40.png) 1340 | > 1341 | > - The next part is to get email and password from input fields and then send them to Firebase. 1342 | 1343 | - Step 5: Replace the "Login Form" component with the following code. 1344 | 1345 | ```js 1346 | function LoginForm() { 1347 | const login = () => { 1348 | console.log("Sign in button was click"); 1349 | }; 1350 | 1351 | const onEmailChanged = (e) => { 1352 | const updatedEmail = e.target.value; 1353 | console.log(`Updated email: ${updatedEmail}`); 1354 | }; 1355 | 1356 | const onPasswordChanged = (e) => { 1357 | const updatedPassword = e.target.value; 1358 | console.log(`Updated password: ${updatedPassword}`); 1359 | }; 1360 | 1361 | return ( 1362 |
1363 |
1364 |

Sign In

1365 |
1366 | 1371 |
1372 |
1373 | 1378 |
1379 | 1382 |
1383 | Remember me 1384 | Need help? 1385 |
1386 |
1387 |
1388 | fb 1392 | Login with Facebook 1393 |
1394 |
1395 | New to Netflix ? 1396 | Sign up now. 1397 |
1398 |
1399 | This page is protected by Google reCAPTCHA to ensure you're not a 1400 | bot. 1401 | Learn more. 1402 |
1403 |
1404 |
1405 |
1406 | ); 1407 | } 1408 | 1409 | export default LoginForm; 1410 | ``` 1411 | 1412 | > 19th NOTE: 1413 | > 1414 | > - "onChanged={onEmailChanged}" was inserted to email field. 1415 | > 1416 | > - "onChanged={onPasswordChanged}" was inserted to password field. 1417 | > 1418 | > - "onEmailChanged" and "onPasswordChanged" were created. 1419 | > 1420 | > - When the current user inputs email. "onEmailChanged" will be triggered. 1421 | > 1422 | > - When the current user inputs password. "onPasswordChanged" will be triggered. 1423 | > 1424 | > - What is "e" ? React provides the concept of **Synthetic Event**. 1425 | > 1426 | > - SyntheticEvent is a cross-browser wrapper around the browser's native event. Its API is the same as the browser's native event, including stopPropagation() and preventDefault(), except the events work identically across all browsers. 1427 | > 1428 | > - The input's value can be get by using **e.target.value** 1429 | > 1430 | > - If you run the project, the result should look like this. 1431 | > 1432 | > ![](./images/41.png) 1433 | > 1434 | > - When the current user is typing an email or password. We need to store those values somewhere else. We should store them in the state. It means that when the user changes the input's values, we will update the corresponding state. 1435 | 1436 | - Step 6: Import "useState" and define "email" and "password" state. 1437 | 1438 | ```js 1439 | import { useState } from "react"; 1440 | ``` 1441 | 1442 | ```js 1443 | function LoginForm() { 1444 | ... 1445 | const [email, setEmail] = useState(); 1446 | const [password, setPassword] = useState() 1447 | ... 1448 | const onEmailChanged = (e) => { 1449 | const updatedEmail = e.target.value; 1450 | setEmail(() => updatedEmail); 1451 | }; 1452 | 1453 | const onPasswordChanged = (e) => { 1454 | const updatedPassword = e.target.value; 1455 | setPassword(() => updatedPassword); 1456 | }; 1457 | ... 1458 | } 1459 | ``` 1460 | 1461 | > 20th NOTE: 1462 | > 1463 | > - Whenever we access the "email" state and "password" state, we can get the latest value. 1464 | > 1465 | > - The remaining task is to send those values to Firebase when the current user clicks on the "Sign In" button. 1466 | 1467 | - Step 7: Import "firebaseAuth" from "firebase.js" file and then update "login" function to send "email" state and "password" state to Firebase. 1468 | 1469 | ```js 1470 | import { firebaseAuth } from "../../firebase/firebase"; 1471 | ``` 1472 | 1473 | ```js 1474 | const login = () => { 1475 | firebaseAuth 1476 | .signInWithEmailAndPassword(email, password) 1477 | .then((userCredential) => { 1478 | // Signed in 1479 | const user = userCredential.user; 1480 | // ... 1481 | console.log(`signed in user`); 1482 | console.log(user); 1483 | }) 1484 | .catch((error) => { 1485 | console.log(error); 1486 | }); 1487 | }; 1488 | ``` 1489 | 1490 | > 21st NOTE: 1491 | > 1492 | > - if email and password are valid. Firebase would return the user's information and vice versa. 1493 | > 1494 | > - You can do anything you want with that value, for example, store the information in localStorage for use later or something like that. 1495 | > 1496 | > - In this case, we are trying to show the user's information on the console log. 1497 | > 1498 | > - If you run the code, your result should be like this. 1499 | > 1500 | > ![](./images/42.png) 1501 | 1502 | 1503 | 1504 | ## **7. Deploy React to Firebase.** 1505 | 1506 | It is time to make our project live on the internet. The following steps will describe how to deploy our application to Firebase. 1507 | 1508 | - Step 1: Change directory to the project's folder and run "npm build" to build the production version of the project. 1509 | 1510 | - Step 2: Go to the Firebase console and choose the "Hosting" option. 1511 | 1512 | ![](./images/43.png) 1513 | 1514 | - Step 3: Click on the "Get started" button. 1515 | 1516 | ![](./images/44.png) 1517 | 1518 | - Step 4: Change directory to the project's folder and run "npm install -g firebase-tools" and then click on the "Next" button. 1519 | 1520 | ![](./images/45.png) 1521 | 1522 | - Step 5: Change directory to the project's folder and run "firebase login" and "fire init". 1523 | 1524 | ![](./images/46.png) 1525 | 1526 | ![](./images/47.png) 1527 | 1528 | - Step 6: Deploy the application to Firebase by running "firebase deploy". 1529 | 1530 | ![](./images/48.png) 1531 | 1532 | ![](./images/49.png) 1533 | 1534 | > Final NOTE: 1535 | > 1536 | > - Now you can access the URL on cmd to view your result. 1537 | 1538 | 1539 | 1540 | ## **8. Summary** 1541 | 1542 | - JSX (Javascript XML): helps us to write Javascript along with HTML. 1543 | 1544 | - React Elements: describe what would be appeared on the screen. 1545 | 1546 | - React Component: Small pieces of UI which could be reusable and combined to build the application. 1547 | 1548 | - useEffect: performs side effects in the application, for example, interacting with the APIs, executing asynchronous operations, and so on. 1549 | 1550 | - useState: defines the state in the application. 1551 | 1552 | - Props: passed from the parent component to the child component, its syntax is similar to HTML attribute. 1553 | 1554 | - React Router: useful routing library that can be used to navigate between pages. 1555 | 1556 | Thank you so much for taking the course. I hope that you could understand important concepts in React and you can build many real-life projects by using React (as front-end) and Firebase (as back-end) to solve many problems and make our life become better. 1557 | 1558 | 1559 | 1560 | ## **9. Useful Resources.** 1561 | 1562 | [1]. https://reactjs.org/docs/getting-started.html. 1563 | 1564 | 1565 | 1566 | ## **10. References** 1567 | 1568 | [1]. https://reactjs.org/docs/getting-started.html. \ 1569 | [2]. https://firebase.google.com/docs/database. \ 1570 | [3]. https://firebase.google.com/docs/auth/web/password-auth. \ 1571 | [4]. https://firebase.google.com/docs/hosting. 1572 | -------------------------------------------------------------------------------- /source/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_FIREBASE_API_KEY = xxx - xxx - xxx - xxx - xxx; 2 | REACT_APP_FIREBASE_AUTH_DOMAIN = xxx - xxx - xxx - xxx - xxx; 3 | REACT_APP_FIREBASE_DATABASE_UTL = xxx - xxx - xxx - xxx - xxx; 4 | REACT_APP_FIREBASE_STORAGE_BUCKET = xxx - xxx - xxx - xxx - xxx; 5 | -------------------------------------------------------------------------------- /source/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/react 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=react 3 | 4 | ### react ### 5 | .DS_* 6 | *.log 7 | logs 8 | **/*.backup.* 9 | **/*.back.* 10 | 11 | node_modules 12 | bower_components 13 | 14 | *.sublime* 15 | 16 | psd 17 | thumb 18 | sketch 19 | 20 | # End of https://www.toptal.com/developers/gitignore/api/react 21 | -------------------------------------------------------------------------------- /source/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "source", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "firebase": "8.6.3", 9 | "react": "17.0.2", 10 | "react-dom": "17.0.2", 11 | "react-router-dom": "5.2.0", 12 | "react-scripts": "4.0.0" 13 | }, 14 | "devDependencies": { 15 | "@babel/runtime": "7.13.8", 16 | "typescript": "4.1.3" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject" 23 | }, 24 | "browserslist": [ 25 | ">0.2%", 26 | "not dead", 27 | "not ie <= 11", 28 | "not op_mini all" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /source/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /source/src/App.js: -------------------------------------------------------------------------------- 1 | // import react router dom. 2 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 3 | // import custom components. 4 | import Home from "./components/home/Home"; 5 | import Login from "./components/login/Login"; 6 | // import global styling. 7 | import "./index.css"; 8 | // create App components. 9 | function App() { 10 | return ( 11 | 12 | 13 | {/* Home Route */} 14 | 15 | 16 | 17 | {/* End Home Route */} 18 | {/* Login Route */} 19 | 20 | 21 | 22 | {/* End Login Route */} 23 | 24 | 25 | ); 26 | } 27 | // export App component. 28 | export default App; 29 | -------------------------------------------------------------------------------- /source/src/components/header/Header.js: -------------------------------------------------------------------------------- 1 | function Header() { 2 | return ( 3 |
4 | {/* Banner Content */} 5 |
6 |

Ginny & Georgia

7 |
8 | 9 | 10 |
11 |

12 | Angsty and awkward fifteen year old Ginny Miller often feels more 13 | mature than her thirty year old mother, the irresistible and dynamic 14 | Georgia Miller... 15 |

16 |
17 | {/* End Banner Content */} 18 | {/* Banner Fade Bottom Animation */} 19 |
20 | {/* End Banner Fade Bottom Animation */} 21 |
22 | ); 23 | } 24 | // export Header component. 25 | export default Header; 26 | -------------------------------------------------------------------------------- /source/src/components/home/Home.js: -------------------------------------------------------------------------------- 1 | // import custom components. 2 | import Nav from "../nav/Nav"; 3 | import Header from "../header/Header"; 4 | import Row from "../row/Row"; 5 | /** 6 | * create Home component. 7 | */ 8 | function Home() { 9 | return ( 10 | <> 11 |
12 | {/* Nav */} 13 |
28 | 29 | ); 30 | } 31 | // export Home component. 32 | export default Home; 33 | -------------------------------------------------------------------------------- /source/src/components/login/Login.js: -------------------------------------------------------------------------------- 1 | // import custom components. 2 | import Nav from "../nav/Nav"; 3 | import LoginForm from "./LoginForm"; 4 | /** 5 | * create Login component. 6 | */ 7 | function Login() { 8 | return ( 9 |
10 | {/* Nav */} 11 |
17 | ); 18 | } 19 | // export Login component. 20 | export default Login; 21 | -------------------------------------------------------------------------------- /source/src/components/login/LoginForm.js: -------------------------------------------------------------------------------- 1 | // import react. 2 | import { useState } from "react"; 3 | // import firebase authentication. 4 | import { firebaseAuth } from "../../firebase/firebase"; 5 | /** 6 | * create LoginForm component. 7 | */ 8 | function LoginForm() { 9 | // create email and password state to store user's credentials. 10 | const [email, setEmail] = useState(); 11 | const [password, setPassword] = useState(); 12 | 13 | /** 14 | * handle event when the user clicks on "Login" button. 15 | */ 16 | const login = () => { 17 | // call firebase authentication service. 18 | firebaseAuth 19 | .signInWithEmailAndPassword(email, password) 20 | .then((userCredential) => { 21 | // Signed in 22 | const user = userCredential.user; 23 | // ... 24 | console.log(`signed in user`); 25 | console.log(user); 26 | }) 27 | .catch((error) => { 28 | console.log(error); 29 | }); 30 | }; 31 | 32 | /** 33 | * update email state when the user inputs the email field. 34 | * @param {*} e - synthetic event to get the latest email's value. 35 | */ 36 | const onEmailChanged = (e) => { 37 | // get email value. 38 | const updatedEmail = e.target.value; 39 | // update email state. 40 | setEmail(() => updatedEmail); 41 | }; 42 | 43 | /** 44 | * update password state when the user input the password field. 45 | * @param {*} e - synthetic event to get the latest password's value. 46 | */ 47 | const onPasswordChanged = (e) => { 48 | // get password value. 49 | const updatedPassword = e.target.value; 50 | // update password state. 51 | setPassword(() => updatedPassword); 52 | }; 53 | 54 | return ( 55 |
56 |
57 |

Sign In

58 |
59 | 64 |
65 |
66 | 71 |
72 | 75 |
76 | Remember me 77 | Need help? 78 |
79 |
80 |
81 | fb 85 | Login with Facebook 86 |
87 |
88 | New to Netflix ? 89 | Sign up now. 90 |
91 |
92 | This page is protected by Google reCAPTCHA to ensure you're not a 93 | bot. 94 | Learn more. 95 |
96 |
97 |
98 |
99 | ); 100 | } 101 | // export LoginForm component. 102 | export default LoginForm; 103 | -------------------------------------------------------------------------------- /source/src/components/nav/Nav.js: -------------------------------------------------------------------------------- 1 | function Nav() { 2 | return ( 3 |
4 | 9 | 14 |
15 | ); 16 | } 17 | // export Nav component. 18 | export default Nav; 19 | -------------------------------------------------------------------------------- /source/src/components/row/Row.js: -------------------------------------------------------------------------------- 1 | // import react. 2 | import { useEffect, useState } from "react"; 3 | // import firebase database. 4 | import { firebaseDatabase } from "../../firebase/firebase"; 5 | /** 6 | * create Row component. 7 | * @param {*} props which are passed to the Row component. 8 | */ 9 | function Row(props) { 10 | // create "movies" state to store list of movies from Firebase. 11 | const [movies, setMovies] = useState([]); 12 | // get props. 13 | const { title, movieType } = props; 14 | // leafRoot to get data from Firebase. 15 | const leafRoot = "movies"; 16 | 17 | /** 18 | * fetch movies from Firebase when getting "movieType" prop. 19 | */ 20 | useEffect(() => { 21 | fetchMovies(movieType); 22 | }, [movieType]); 23 | 24 | /** 25 | * fetch movies from Firebase. 26 | * @param {*} movieType which is used to get movies from Firebase. 27 | */ 28 | const fetchMovies = (movieType) => { 29 | const movieRef = firebaseDatabase.ref(`${leafRoot}/${movieType}`); 30 | movieRef.on("value", (snapshot) => { 31 | const movies = snapshot.val(); 32 | if (movies && movies.length !== 0) { 33 | // update "movies" state after getting movies from Firebase. 34 | setMovies(() => movies); 35 | } 36 | }); 37 | }; 38 | 39 | return ( 40 |
41 | {/* Title */} 42 |

{title}

43 | {/* End Title */} 44 | {/* List of Movies */} 45 |
46 | {movies.map((movie) => ( 47 | {movie.original_name} 53 | ))} 54 |
55 | {/* End List of Movies */} 56 |
57 | ); 58 | } 59 | // export Row component. 60 | export default Row; 61 | -------------------------------------------------------------------------------- /source/src/firebase/firebase.js: -------------------------------------------------------------------------------- 1 | // import firebase. 2 | import firebase from "firebase"; 3 | // initialize firebase app in the application. 4 | const app = firebase.initializeApp({ 5 | apiKey: `${process.env.REACT_APP_FIREBASE_API_KEY}`, 6 | authDomain: `${process.env.REACT_APP_FIREBASE_AUTH_DOMAIN}`, 7 | // For databases not in the us-central1 location, databaseURL will be of the 8 | // form https://[databaseName].[region].firebasedatabase.app. 9 | // For example, https://your-database-123.europe-west1.firebasedatabase.app 10 | databaseURL: `${process.env.REACT_APP_FIREBASE_DATABASE_URL}`, 11 | storageBucket: `${process.env.REACT_APP_FIREBASE_STORAGE_BUCKET}`, 12 | }); 13 | // create firebase database. 14 | const firebaseDatabase = app.database(); 15 | // create firebase auth. 16 | const firebaseAuth = app.auth(); 17 | // export firebase database and firebase auth for later use. 18 | export { firebaseDatabase, firebaseAuth }; 19 | -------------------------------------------------------------------------------- /source/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 3 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 4 | sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | margin: 0; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | 15 | * { 16 | margin: 0; 17 | } 18 | 19 | .app { 20 | background-color: #111; 21 | } 22 | 23 | .row { 24 | color: #fff; 25 | margin-left: 20px; 26 | } 27 | 28 | .row__posters { 29 | display: flex; 30 | overflow-x: scroll; 31 | overflow-y: hidden; 32 | padding: 20px 0 20px 0; 33 | } 34 | 35 | .row__posters::-webkit-scrollbar { 36 | display: none; 37 | } 38 | 39 | .row__poster { 40 | margin-right: 10px; 41 | max-height: 100px; 42 | object-fit: contain; 43 | opacity: 0.8; 44 | transition: transform 0.45s; 45 | width: 100%; 46 | } 47 | 48 | .row__poster:hover { 49 | cursor: pointer; 50 | opacity: 1; 51 | transform: scale(1.08); 52 | } 53 | 54 | .row__posterLarge { 55 | max-height: 250px; 56 | } 57 | 58 | .row__posterLarge:hover { 59 | opacity: 1; 60 | transform: scale(1.09); 61 | } 62 | 63 | .banner { 64 | background-image: url("https://image.tmdb.org/t/p/original//hVMoqvXs5j9ffun56tZ5YhliSD9.jpg"); 65 | background-position: center center; 66 | background-size: cover; 67 | color: #fff; 68 | height: 448px; 69 | object-fit: contain; 70 | } 71 | 72 | .banner__contents { 73 | height: 190px; 74 | margin-left: 30px; 75 | padding-top: 140px; 76 | } 77 | 78 | .banner__title { 79 | font-size: 3rem; 80 | font-weight: 800; 81 | padding-bottom: 0.3rem; 82 | } 83 | 84 | .banner__description { 85 | font-size: 16px; 86 | height: 80px; 87 | line-height: 1.3; 88 | line-height: 24px; 89 | max-width: 360px; 90 | padding-top: 1rem; 91 | width: 45rem; 92 | } 93 | 94 | .banner__button { 95 | background-color: rgba(51, 51, 51, 0.5); 96 | border: none; 97 | border-radius: 0.2vw; 98 | color: #fff; 99 | cursor: pointer; 100 | font-weight: 700; 101 | margin-right: 1rem; 102 | outline: none; 103 | padding: 0.5rem 2rem; 104 | } 105 | 106 | .banner__button:hover { 107 | background-color: #e6e6e6; 108 | color: #000; 109 | transition: all 0.2s; 110 | } 111 | 112 | .banner--fadeBottom { 113 | background-image: linear-gradient( 114 | 180deg, 115 | transparent, 116 | rgba(37, 37, 37, 0.61), 117 | #111 118 | ); 119 | height: 7.4rem; 120 | } 121 | 122 | .nav { 123 | background: #111; 124 | display: flex; 125 | height: 30px; 126 | justify-content: space-between; 127 | padding: 20px; 128 | position: fixed; 129 | top: 0; 130 | transition: all 0.5s; 131 | transition-timing-function: ease-in; 132 | width: 100%; 133 | z-index: 1; 134 | } 135 | 136 | .nav__black { 137 | background-color: #111; 138 | } 139 | 140 | .nav__logo { 141 | left: 20px; 142 | width: 80px; 143 | } 144 | 145 | .nav__avatar, 146 | .nav__logo { 147 | object-fit: contain; 148 | position: fixed; 149 | } 150 | 151 | .nav__avatar { 152 | right: 20px; 153 | width: 30px; 154 | } 155 | 156 | .main { 157 | background: url("https://assets.nflxext.com/ffe/siteui/vlv3/11c803d4-06f3-467e-91f7-0560ce15a396/5ac22c62-f87e-45e3-8746-418d647185c9/VN-en-20210517-popsignuptwoweeks-perspective_alpha_website_small.jpg"); 158 | background-size: cover; 159 | min-height: 100vh; 160 | } 161 | 162 | .login-body { 163 | align-items: center; 164 | display: grid; 165 | justify-content: center; 166 | min-height: 100vh; 167 | } 168 | 169 | .login-body__form { 170 | background-color: rgba(0, 0, 0, 0.8); 171 | box-sizing: border-box; 172 | margin-top: 62px; 173 | max-width: 420px; 174 | min-width: 420px; 175 | padding: 48px 62px; 176 | } 177 | 178 | .login-body h1 { 179 | color: #fff; 180 | font-size: 32px; 181 | font-weight: 700; 182 | margin-bottom: 28px; 183 | } 184 | 185 | .login-body__input input { 186 | background: #333; 187 | border: 0; 188 | border-radius: 4px; 189 | color: #fff; 190 | font-size: 16px; 191 | outline: none; 192 | padding: 16px 20px; 193 | width: 86%; 194 | } 195 | 196 | .mb-16 { 197 | margin-bottom: 16px; 198 | } 199 | 200 | .login-body__submit-btn { 201 | background: #e50914; 202 | border: 0; 203 | border-radius: 4px; 204 | color: #fff; 205 | font-size: 16px; 206 | font-weight: bold; 207 | margin-top: 32px; 208 | outline: none; 209 | padding: 16px; 210 | width: 100%; 211 | } 212 | 213 | .login-body__submit-btn:hover { 214 | cursor: pointer; 215 | } 216 | 217 | .login-body__options { 218 | color: #b3b3b3; 219 | display: grid; 220 | font-size: 13px; 221 | font-weight: 500; 222 | grid-template-columns: repeat(2, 1fr); 223 | margin-bottom: 48px; 224 | margin-top: 12px; 225 | } 226 | 227 | .login-body__options:hover { 228 | cursor: pointer; 229 | } 230 | 231 | .login-body__need-help { 232 | text-align: right; 233 | } 234 | 235 | .login-body__fb { 236 | align-items: center; 237 | color: #b3b3b3; 238 | display: grid; 239 | font-size: 13px; 240 | grid-template-columns: min-content auto; 241 | } 242 | 243 | .login-body__fb:hover { 244 | cursor: pointer; 245 | } 246 | 247 | .login-body__fb img { 248 | height: 20px; 249 | margin-right: 12px; 250 | width: 20px; 251 | } 252 | 253 | .login-body__new-to-nl { 254 | color: #737373; 255 | font-size: 16px; 256 | font-weight: 500; 257 | margin-top: 16px; 258 | } 259 | 260 | .login-body__sign-up { 261 | color: #fff; 262 | } 263 | 264 | .login-body__sign-up:hover { 265 | cursor: pointer; 266 | } 267 | 268 | .login-body__google_captcha { 269 | color: #8c8c8c; 270 | font-size: 13px; 271 | margin-top: 16px; 272 | } 273 | 274 | .login-body__learn-more { 275 | color: #0071eb; 276 | } 277 | 278 | .login-body__learn-more:hover { 279 | cursor: pointer; 280 | } 281 | 282 | .detail { 283 | background: #111; 284 | bottom: 0; 285 | position: fixed; 286 | right: 0; 287 | top: 0; 288 | width: 450px; 289 | z-index: 999; 290 | } 291 | 292 | .detail__image { 293 | align-items: end; 294 | background-position: center center; 295 | background-size: cover; 296 | display: grid; 297 | height: 282px; 298 | width: 100%; 299 | } 300 | 301 | .detail__title { 302 | color: #fff; 303 | font-size: 2rem; 304 | font-weight: bold; 305 | padding-bottom: 24px; 306 | padding-left: 24px; 307 | z-index: 3; 308 | } 309 | 310 | .detail__fade-image { 311 | position: absolute; 312 | top: 164px; 313 | width: 100%; 314 | } 315 | 316 | .detail__actions { 317 | padding: 12px; 318 | padding-left: 24px; 319 | } 320 | 321 | .detail__btn { 322 | background: #e50914; 323 | border: none; 324 | border-radius: 4px; 325 | color: #fff; 326 | font-weight: 600; 327 | outline: none; 328 | padding: 8px 32px; 329 | } 330 | 331 | .detail__btn:hover { 332 | background: #e74c3c; 333 | } 334 | 335 | .detail__btn:hover { 336 | cursor: pointer; 337 | } 338 | 339 | .detail__description { 340 | color: #fff; 341 | padding: 12px; 342 | padding-left: 24px; 343 | padding-right: 24px; 344 | text-align: justify; 345 | } 346 | 347 | .mgr-8 { 348 | margin-right: 8px; 349 | } 350 | 351 | .detail__description-title { 352 | border-bottom: 2px solid #e50914; 353 | display: inline-block; 354 | font-weight: 600; 355 | margin-bottom: 12px; 356 | padding: 12px 0px; 357 | } 358 | 359 | .detail__description-content { 360 | font-size: 14px; 361 | } 362 | -------------------------------------------------------------------------------- /source/src/index.js: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import ReactDOM from "react-dom"; 3 | // import App component. 4 | import App from "./App"; 5 | // replace App component to the root document. 6 | const rootElement = document.getElementById("root"); 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | rootElement 12 | ); 13 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Netflix 8 | 9 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 31 | 32 | 33 | 55 | 56 |
57 |

NETFLIX ORGINALS

58 |
59 | Jupiter's LegacyLuciferLuis Miguel: The SeriesSelena: The SeriesWho Killed Sara?Love, Death & RobotsCobra KaiEliteStranger ThingsMoney HeistThe Umbrella AcademyHaunted: Latin AmericaLupinChilling Adventures of SabrinaCastlevaniaGinny & GeorgiaSex EducationHalstonDarkThe Innocent 140 |
141 |
142 |
143 |

Trending Now

144 |
145 | Jupiter's LegacyThe Bad BatchThe Falcon and the Winter SoldierShadow and Bone 210 |
211 |
212 |
213 |

Top Rated

214 |
215 | 276 |
277 |
278 |
279 |

Action Movies

280 |
281 | 342 |
343 |
344 |
345 |

Horror Movies

346 |
347 | 408 |
409 |
410 |
411 |

Romance Movies

412 |
413 | 474 |
475 |
476 |
477 |

Documentaries Movies

478 |
479 | 540 |
541 |
542 |
543 |
544 | 549 | 554 | 558 | 572 | 573 | 574 | -------------------------------------------------------------------------------- /ui/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Netflix - Signin 8 | 9 | 14 | 15 | 16 |
17 |
18 | 19 | 30 | 31 | 32 | 66 | 67 |
68 |
69 | 70 | 71 | -------------------------------------------------------------------------------- /ui/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 3 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 4 | sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | margin: 0; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | 15 | * { 16 | margin: 0; 17 | } 18 | 19 | .app { 20 | background-color: #111; 21 | } 22 | 23 | .row { 24 | color: #fff; 25 | margin-left: 20px; 26 | } 27 | 28 | .row__posters { 29 | display: flex; 30 | overflow-x: scroll; 31 | overflow-y: hidden; 32 | padding: 20px 0 20px 0; 33 | } 34 | 35 | .row__posters::-webkit-scrollbar { 36 | display: none; 37 | } 38 | 39 | .row__poster { 40 | margin-right: 10px; 41 | max-height: 100px; 42 | object-fit: contain; 43 | opacity: 0.8; 44 | transition: transform 0.45s; 45 | width: 100%; 46 | } 47 | 48 | .row__poster:hover { 49 | cursor: pointer; 50 | opacity: 1; 51 | transform: scale(1.08); 52 | } 53 | 54 | .row__posterLarge { 55 | max-height: 250px; 56 | } 57 | 58 | .row__posterLarge:hover { 59 | opacity: 1; 60 | transform: scale(1.09); 61 | } 62 | 63 | .banner { 64 | background-image: url("https://image.tmdb.org/t/p/original//hVMoqvXs5j9ffun56tZ5YhliSD9.jpg"); 65 | background-position: center center; 66 | background-size: cover; 67 | color: #fff; 68 | height: 448px; 69 | object-fit: contain; 70 | } 71 | 72 | .banner__contents { 73 | height: 190px; 74 | margin-left: 30px; 75 | padding-top: 140px; 76 | } 77 | 78 | .banner__title { 79 | font-size: 3rem; 80 | font-weight: 800; 81 | padding-bottom: 0.3rem; 82 | } 83 | 84 | .banner__description { 85 | font-size: 16px; 86 | height: 80px; 87 | line-height: 1.3; 88 | line-height: 24px; 89 | max-width: 360px; 90 | padding-top: 1rem; 91 | width: 45rem; 92 | } 93 | 94 | .banner__button { 95 | background-color: rgba(51, 51, 51, 0.5); 96 | border: none; 97 | border-radius: 0.2vw; 98 | color: #fff; 99 | cursor: pointer; 100 | font-weight: 700; 101 | margin-right: 1rem; 102 | outline: none; 103 | padding: 0.5rem 2rem; 104 | } 105 | 106 | .banner__button:hover { 107 | background-color: #e6e6e6; 108 | color: #000; 109 | transition: all 0.2s; 110 | } 111 | 112 | .banner--fadeBottom { 113 | background-image: linear-gradient( 114 | 180deg, 115 | transparent, 116 | rgba(37, 37, 37, 0.61), 117 | #111 118 | ); 119 | height: 7.4rem; 120 | } 121 | 122 | .nav { 123 | background: #111; 124 | display: flex; 125 | height: 30px; 126 | justify-content: space-between; 127 | padding: 20px; 128 | position: fixed; 129 | top: 0; 130 | transition: all 0.5s; 131 | transition-timing-function: ease-in; 132 | width: 100%; 133 | z-index: 1; 134 | } 135 | 136 | .nav__black { 137 | background-color: #111; 138 | } 139 | 140 | .nav__logo { 141 | left: 20px; 142 | width: 80px; 143 | } 144 | 145 | .nav__avatar, 146 | .nav__logo { 147 | object-fit: contain; 148 | position: fixed; 149 | } 150 | 151 | .nav__avatar { 152 | right: 20px; 153 | width: 30px; 154 | } 155 | 156 | .main { 157 | background: url("https://assets.nflxext.com/ffe/siteui/vlv3/11c803d4-06f3-467e-91f7-0560ce15a396/5ac22c62-f87e-45e3-8746-418d647185c9/VN-en-20210517-popsignuptwoweeks-perspective_alpha_website_small.jpg"); 158 | background-size: cover; 159 | min-height: 100vh; 160 | } 161 | 162 | .login-body { 163 | align-items: center; 164 | display: grid; 165 | justify-content: center; 166 | min-height: 100vh; 167 | } 168 | 169 | .login-body__form { 170 | background-color: rgba(0, 0, 0, 0.8); 171 | box-sizing: border-box; 172 | margin-top: 62px; 173 | max-width: 420px; 174 | min-width: 420px; 175 | padding: 48px 62px; 176 | } 177 | 178 | .login-body h1 { 179 | color: #fff; 180 | font-size: 32px; 181 | font-weight: 700; 182 | margin-bottom: 28px; 183 | } 184 | 185 | .login-body__input input { 186 | background: #333; 187 | border: 0; 188 | border-radius: 4px; 189 | color: #fff; 190 | font-size: 16px; 191 | outline: none; 192 | padding: 16px 20px; 193 | width: 86%; 194 | } 195 | 196 | .mb-16 { 197 | margin-bottom: 16px; 198 | } 199 | 200 | .login-body__submit-btn { 201 | background: #e50914; 202 | border: 0; 203 | border-radius: 4px; 204 | color: #fff; 205 | font-size: 16px; 206 | font-weight: bold; 207 | margin-top: 32px; 208 | outline: none; 209 | padding: 16px; 210 | width: 100%; 211 | } 212 | 213 | .login-body__submit-btn:hover { 214 | cursor: pointer; 215 | } 216 | 217 | .login-body__options { 218 | color: #b3b3b3; 219 | display: grid; 220 | font-size: 13px; 221 | font-weight: 500; 222 | grid-template-columns: repeat(2, 1fr); 223 | margin-bottom: 48px; 224 | margin-top: 12px; 225 | } 226 | 227 | .login-body__options:hover { 228 | cursor: pointer; 229 | } 230 | 231 | .login-body__need-help { 232 | text-align: right; 233 | } 234 | 235 | .login-body__fb { 236 | align-items: center; 237 | color: #b3b3b3; 238 | display: grid; 239 | font-size: 13px; 240 | grid-template-columns: min-content auto; 241 | } 242 | 243 | .login-body__fb:hover { 244 | cursor: pointer; 245 | } 246 | 247 | .login-body__fb img { 248 | height: 20px; 249 | margin-right: 12px; 250 | width: 20px; 251 | } 252 | 253 | .login-body__new-to-nl { 254 | color: #737373; 255 | font-size: 16px; 256 | font-weight: 500; 257 | margin-top: 16px; 258 | } 259 | 260 | .login-body__sign-up { 261 | color: #fff; 262 | } 263 | 264 | .login-body__sign-up:hover { 265 | cursor: pointer; 266 | } 267 | 268 | .login-body__google_captcha { 269 | color: #8c8c8c; 270 | font-size: 13px; 271 | margin-top: 16px; 272 | } 273 | 274 | .login-body__learn-more { 275 | color: #0071eb; 276 | } 277 | 278 | .login-body__learn-more:hover { 279 | cursor: pointer; 280 | } 281 | 282 | .mgr-8 { 283 | margin-right: 8px; 284 | } 285 | --------------------------------------------------------------------------------