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
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
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
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: {
}
})
[ 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>
Now, we are ready to create all components π
[ 2.1 ] Components
Let's create three components :
components
|--> Footer.vue
|--> LogoTesla.vue
|--> Section.vue
# ~/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>
# ~/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>
# ~/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
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>
# ~/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 π
Top comments (0)