DEV Community

Cover image for How Build Tesla's Home Page  Clone with Vue.js [ Series - Portfolio Apps ]
Sith Norvang
Sith Norvang

Posted on

How Build Tesla's Home Page Clone with Vue.js [ Series - Portfolio Apps ]

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

2.0 / Components & Router

[ 1.1 ] Use Preset Configuration

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

Enter fullscreen mode Exit fullscreen mode

Config with preset

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

Enter fullscreen mode Exit fullscreen mode

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

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

Enter fullscreen mode Exit fullscreen mode

Get ready to configure Vuex ?

[ 1.2 ] Vuex

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
# 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: {
  }
})


Enter fullscreen mode Exit fullscreen mode

[ 1.3 ] Config "App.vue"

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

  • NavBar,
  • Sections,
# ~/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>


Enter fullscreen mode Exit fullscreen mode

Now, we are ready to create all components 👍

[ 2.1 ] Components

Let's create three components :

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

Enter fullscreen mode Exit fullscreen mode
# ~/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>

Enter fullscreen mode Exit fullscreen mode
# ~/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>

Enter fullscreen mode Exit fullscreen mode
# ~/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>

Enter fullscreen mode Exit fullscreen mode

[ 2.2 ] View & Router

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

# ~/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>

Enter fullscreen mode Exit fullscreen mode
# ~/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

Enter fullscreen mode Exit fullscreen mode

Done ! You can find the result on link below :

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

See you on next episode 😉

Top comments (0)