How Build Tesla's Home Page  Clone with Vue.js [ Series - Portfolio Apps ]
Tags: vue, clone, portfolio, challenge
Published: 2021-09-29 15:19:24

Hi Guys, it's time launching fourth episode of "Portfolio Apps" series. 

Here, we are going to clone Tesla's Home Page. I decide writing this tutorial because I didn't find a way to combine AOS library and CSS Scroll Snapping. After figure it out. I wanted to share it.

## 1.0 / Setup
  * [ 1.1 | Preset and Configuration ] (#preset-configuration)
  * [ 1.2 | Vuex ] (#vuex)
  * [ 1.3 | Config "App.vue" & "main.js" files ] (#config-files)

## 2.0 / Components & Router
   * [ 2.1 | Components ] (#components)
   * [ 2.2 | View & Router ] (#view-router)

## [ 1.1 ] Use Preset Configuration <a name="preset-configuration"></a>

Let's create a new project call "clone-hp-tesla" and use the previous preset "config-portfolio"

```
# Run this command

vue create clone-hp-tesla

```

![Config with preset](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/11avfvtl2xjymijhvwsp.png)

About the feature of Scroll Snapping, we are going to install one plugin provide by Angelomelonas. Thanks to him 👍

```
# Run this command
# https://github.com/angelomelonas/vue-scroll-snap

npm install vue-scroll-snap --save

```

We need some background image like Tesla official home page. To pick all images, I used a chrome extension name "Image Picker"

https://chrome.google.com/webstore/detail/image-picker/bhibldekjicdbnjeeecmgoogcihoalhe

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vz99gufajignzj3h8njx.png)

After downloading all images, store it in "assets" folder as following :

```
assets
|--> 1-hero-section-model3r.jpeg
|--> 2-section-solar-powerwall.jpeg
|--> 3-section-model-yr.jpeg
|--> 4-section-model-sr.jpeg
|--> 5-section-model-xr.jpeg

```
Get ready to configure Vuex ?

## [ 1.2 ] Vuex <a name="vuex"></a>

This tutorial is about to build animated home page. It means, we don't need creating to complex architecture for the store. 

In folder "store" and "index.js" file, let's create two states / getters :

- sectionMode
- homeSections

One action and mutation : 

- changeSection

```js
# store/index.js

import { createStore } from 'vuex'

export default createStore({
  state: {
    sectionMode: "Model 3R",
    homeSections: [
      { 
        id: 1,
        idSection: "section-one-hero",
        title: "Model 3R",
        paragraph: "Electric vehicule incentives are now available on eligible Model 3 in ACT, NSW, TAS and VIC.", 
        bgImage: require("@/assets/1-hero-section-model-3r.jpeg"),
        buttons: [
          { 
            value: "EXISTING INVENTORY", 
            bgColor: "rgba(30, 30, 30, 0.800)", 
            color: "rgba(249, 249, 249, 1.000)"
          },
          { 
            value: "CUSTOM ORDER", 
            bgColor: "rgba(249, 249, 249, 0.800)"
          }
        ]
      },
      { 
        id: 2, 
        idSection: "section-three-model-yr",
        title: "Model YR", 
        bgImage: require("@/assets/3-section-model-yr.jpeg"),
        buttons: [
          { 
            value: "LEARN MORE", 
            bgColor: "rgba(30, 30, 30, 0.800)", 
            color: "rgba(249, 249, 249, 1.000)"
          },
          { 
            value: "STAY UPDATED",
            bgColor: "rgba(249, 249, 249, 0.800)"
          }
        ]
      },
      { 
        id: 3, 
        idSection: "section-four-model-sr",
        title: "Model SR",
        paragraph: "Schedule a Touchless Test Drive", 
        bgImage: require("@/assets/4-section-model-sr.jpeg"),
        buttons: [
          {
            value: "CUSTOM ORDER",
            bgColor: "rgba(30, 30, 30, 0.800)",
            color: "rgba(249, 249, 249, 1.000)"
          },
          {
            value: "EXISTING INVENTORY",
            bgColor: "rgba(249, 249, 249, 0.800)"
          }
        ]
      },
      { 
        id: 4, 
        idSection: "section-five-model-xr",
        title: "Model XR",
        paragraph: "Schecule a Touchless Test Drive",
        bgImage: require("@/assets/5-section-model-xr.jpeg"),
        buttons: [
          {
            value: "CUSTOM ORDER",
            bgColor: "rgba(30, 30, 30, 0.800)",
            color: "rgba(249, 249, 249, 1.000)"
          },
          {
            value: "EXISTING INVENTORY",
            bgColor: "rgba(249, 249, 249, 0.800)"
          }
        ]
      },
      { 
        id: 5, 
        idSection: "section-two-solar-powerwall",
        title: "Solar and Powerwall",
        paragraph: "Power Everything", 
        bgImage: require("@/assets/2-section-solar-powerwall.jpeg"),
        buttons: [
          {
            value: "LEARN MORE",
            bgColor: "rgba(30, 30, 30, 0.800)",
            color: "rgba(249, 249, 249, 1.000)"
          }
        ]
      },
    ]
  },
  mutations: {
    changeSection(state, payload) {
      state.sectionMode = payload;
    },
  },
  actions: {
    changeSection(context,payload) {
      context.commit("changeSection", payload)
    }
  },
  getters: {
    sectionMode(state) {
      return state.sectionMode
    },
    homeSections(state) {
      return state.homeSections
    }
  },
  modules: {
  }
})


```
## [ 1.3 ] Config "App.vue" <a name="config-files"></a>

In vuex, we created a state which return all data we need :

- NavBar,
- Sections,

```js
# ~/src/App.vue

<template>
  <LogoTesla />
  <div id="nav">
    <a
      v-for="section in homeSections"
      :key="section.id"
      :href="'#' + section.idSection"
      @click="changeSection(section.title)"
    >
      {{ section.title }}
    </a>
  </div>
  <Footer />
  <router-view/>
</template>

<script>
import LogoTesla from "@/components/LogoTesla"
import Footer from "@/components/Footer"

export default {
  components: {
    LogoTesla,
    Footer
  },
  computed: {
    homeSections() {
      return this.$store.getters["homeSections"]
    }
  },
  methods: {
    changeSection(section) {
      setTimeout(() => {
        this.$store.dispatch("changeSection", section);
      },1000);
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  position: fixed;
  z-index: 500;
  display: flex;
  gap: 20px;
  justify-content: center;
  width: 100%;
  padding: 30px;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
}

#nav a.router-link-exact-active {
  color: #42b983;
}

body {
  margin: 0;
}

a {
  text-decoration: none;
}
</style>


```

Now, we are ready to create all components 👍

## [ 2.1 ] Components <a name="components"></a>

Let's create three components :

```
components
|--> Footer.vue
|--> LogoTesla.vue
|--> Section.vue

```

```js
# ~/components/Footer.vue

<template>
  <div class="footer">Tesla's Home Page Clone | Create with ❤️ by Sith Norvang</div>
</template>

<style>
.footer {
  position: fixed;
  display: flex;
  justify-content: center;
  z-index: 500;
  padding: 10px;
  font-size: 14px;
  bottom: 0;
  width: 100%;
  height: 30px;
  display: flex;
  color: white;
  background: rgb(30,30,30);
  background: linear-gradient(0deg, rgba(30,30,30,1) 0%, rgba(30,30,30,0) 100%);
}
</style>

```

```js
# ~/components/LogoTesla.vue

<template>
  <svg
    style="position: absolute; left: 20; top: -10px;" 
    xmlns="http://www.w3.org/2000/svg"
    height="100"
    width="150"
    viewBox="-41.8008 -9.08425 362.2736 54.5055"
  >
    <path 
      d="M238.077 14.382v21.912h7.027V21.705h25.575v14.589h7.022V14.42l-39.624-.038m6.244-7.088h27.02c3.753-.746 6.544-4.058 7.331-7.262h-41.681c.779 3.205 3.611 6.516 7.33 7.262m-27.526 29.014c3.543-1.502 5.449-4.1 6.179-7.14h-31.517l.02-29.118-7.065.02v36.238h32.383M131.874 7.196h24.954c3.762-1.093 6.921-3.959 7.691-7.136h-39.64v21.415h32.444v7.515l-25.449.02c-3.988 1.112-7.37 3.79-9.057 7.327l2.062-.038h39.415V14.355h-32.42V7.196m-61.603.069h27.011c3.758-.749 6.551-4.058 7.334-7.265H62.937c.778 3.207 3.612 6.516 7.334 7.265m0 14.322h27.011c3.758-.741 6.551-4.053 7.334-7.262H62.937c.778 3.21 3.612 6.521 7.334 7.262m0 14.717h27.011c3.758-.747 6.551-4.058 7.334-7.263H62.937c.778 3.206 3.612 6.516 7.334 7.263M0 .088c.812 3.167 3.554 6.404 7.316 7.215h11.37l.58.229v28.691h7.1V7.532l.645-.229h11.38c3.804-.98 6.487-4.048 7.285-7.215v-.07H0v.07"
    />
</svg>
</template>

<script>
export default {
  name: "LogoTesla"
}
</script>

```

```js
# ~/components/Section.vue

<template>
  <div
    :id="idSection"
    class="section-container"
    :style="{ backgroundImage:`url(${bgImage})` }"
    @mouseover="changeSection"
  >
    <transition name="fade" mode="out-in">
      <div class="data-container" v-if="sectionMode === title">
        <div class="text-container">
          <label>{{ title }}</label>
          <p>{{ paragraph }}</p>
        </div>
        <div class="button-container">
          <button 
            v-for="(button, index) in buttons" 
            :key="index"
            :style="{ backgroundColor: button.bgColor, color: button.color }"
            @click="toDevTo"
          >
            {{ button.value }}
          </button>
        </div>
      </div>
    </transition>
  </div>
</template>

<script>
export default {
  name: 'Section',
  props: [ "idSection", "title", "paragraph", "bgImage", "buttons"],
  computed: {
    sectionMode() {
      return this.$store.getters["sectionMode"]
    }
  },
  methods: {
    changeSection() {
      this.$store.dispatch("changeSection", this.title)
    },
    toDevTo() {
    window.location.href = "https://dev.to/sithcode/step-by-step-guide-to-build-vue-js-apps-for-your-portfolio-meditation-app-24n6"
    }
  },
}
</script>

<style>
.section-container {
  display: flex;
  justify-content: center;
  background-size: cover;
  background-position: center;
  object-fit: cover;
  padding: 100px 0px 0px 0px;
}

.data-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 80%;
  height: 90%;
}

.text-container {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.button-container {
  display: flex;
  flex-direction: row;
  gap: 20px;
}

label {
  font-weight: bold;
  font-size: 40px;
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}


.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

button {
  display: flex;
  padding: 12.5px 70px 12.5px 70px;
  border-radius: 20px;
  border: none;
  font-weight: 550;
  cursor: pointer;
}
</style>

```
## [ 2.2 ] View & Router <a name="view-router"></a>

This is the last step. We need to create the view of our home page and configure our router.

```js
# ~/views/ViewHome.vue

<template>
  <VueScrollSnap>
    <Section 
      v-for="section in homeSections"
      :key="section.id"
      :idSection="section.idSection"
      :title="section.title"
      :paragraph="section.paragraph"
      :bgImage="section.bgImage"
      :buttons="section.buttons"
      class="item"
    />
  </VueScrollSnap>
</template>

<script>
  import VueScrollSnap from "vue-scroll-snap";
  import Section from "@/components/Section"

  export default {
    name: "ViewHome",
    components: {
      VueScrollSnap,
      Section
    },
    computed: {
      homeSections() {
        return this.$store.getters["homeSections"]
      },
    },
  };
</script>

<style>
.item {
  height: calc(100vh - 100px);
}

.scroll-snap-container {
  height: 100vh;
  width: 100vw;
}
</style>

```

```js
# ~/router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import ViewHome from '../views/ViewHome.vue'

const routes = [
  {
    path: '/',
    name: 'ViewHome',
    component: ViewHome
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

```
Done ! You can find the result on link below :

https://tesla-clone-home-page.netlify.app/

See you on next episode 😉