In this article, I’m going to demonstrate how I built an iOS home screen clone using just Vue.js.
First, let’s have a look at the project structure:
…..favicon.ico
…..index.html
?src
……?data
……….contacts.js
……?components
……….ClockView.vue
……….GalleryView.vue
……….HomeWidget.vue
……….MainWrapper.vue
……….NotesView.vue
……….PhoneView.vue
…App.vue
…main.js
package.json
Project files
And here’s what each of these components are supposed to do:
MainWrapper.vue: This is the wrapper component which which houses all the other components. I’ve also kept the bottom dock icons inside this component.
ClockView.vue: I’m keeping the clock / time and date details inside this component.
GalleryView.vue: This is where I’m keeping the collection of photos, which I generate randomly upon every component visibility toggle.
HomeWidget.vue: It’ll only show the current time on the home screen
NotesView.vue: This is where you can create and save notes. Note that this NotesView is a temporary holding space and doesn’t actually saves your notes.
PhoneView.vue: This is where I’ve created a list of seeded contacts, which you can hover over to see their details
data/contacts.js: Contains seed data for the contacts list information
MainWrapper component
Now, here’s the code for the MainWrapper:
<template>
<div class="np-wrapper-container">
<div v-if="selectedView == selectableViews.home">
<HomeWidget />
</div>
<div v-if="selectedView == selectableViews.phone">
<PhoneView />
</div>
<div v-if="selectedView == selectableViews.notes">
<NotesView />
</div>
<div v-if="selectedView == selectableViews.gallery">
<GalleryView />
</div>
<div v-if="selectedView == selectableViews.clock">
<ClockView />
</div>
<div class="np-icons-wrapper">
<div class="np-icons-holder" @click="selectView(selectPhone)">
<img src="https://i.ibb.co/PzLbkMY/phone-2.png" />
</div>
<div class="np-icons-holder" @click="selectView(selectNotes)">
<img src="https://i.ibb.co/PwhFXd2/notes.png" />
</div>
<div class="np-icons-holder" @click="selectView(selectGallery)">
<img src="https://i.ibb.co/b2J7MtF/photos.png" />
</div>
<div class="np-icons-holder" @click="selectView(selectClock)">
<img src="https://i.ibb.co/m0cg5wS/clock.png" />
</div>
</div>
</div>
</template>
<script>
import PhoneView from "./PhoneView";
import NotesView from "./NotesView";
import GalleryView from "./GalleryView";
import ClockView from "./ClockView";
import HomeWidget from "./HomeWidget";
export default {
name: "MainWrapper",
components: {
PhoneView,
NotesView,
GalleryView,
ClockView,
HomeWidget,
},
data() {
return {
selectedView: -1,
selectableViews: {
home: -1,
phone: 0,
notes: 1,
gallery: 2,
clock: 3,
},
selectHome: -1,
selectPhone: 0,
selectNotes: 1,
selectGallery: 2,
selectClock: 3,
};
},
methods: {
selectView(viewToSelect) {
if (this.selectedView === viewToSelect) {
this.selectedView = this.selectHome;
} else {
this.selectedView = viewToSelect;
}
},
},
};
</script>
<style>
.np-wrapper-container {
position: relative;
height: 500px;
width: 250px;
border: 1px solid #eee;
background: url(https://i.ibb.co/pjPFT82/iphone-13-wallpaper.jpg);
}
.np-icons-wrapper {
position: absolute;
height: 54px;
width: 240px;
margin: 0 5px;
background: rgba(238, 238, 238, 0.568);
bottom: 0;
border-radius: 6px;
transform: translateY(-6px);
}
.np-icons-holder {
display: inline-block;
width: 24%;
height: 50px;
transform: translateY(3px) translateX(4px) scale(0.9);
text-align: center;
transition: all 0.3s;
cursor: pointer;
}
.np-icons-holder:hover {
transform: translateY(3px) scale(1);
transition: all 0.3s;
}
</style>
Code language: HTML, XML (xml)
In the MainWrapper.vue, as you can see, on line 54, I’ve created an enum for selectable views. I’ve then assigned the value of a selection on line 70, whenever you click on an icon. You’d also notice that if you click on Gallery when the Gallery is already opened, you’ll be forced to see the home view instead. I’ve done so on line no 71, this also acts as a minimise / maximise functionality.
PhoneView component
Let’s see PhoneView.vue now:
<template>
<div class="np-app-container">
<div class="np-app-title">Contacts</div>
<div style="padding-top: 10px">
<div v-for="(contact, c) in contacts" :key="c">
<div class="np-contact-name" @mouseover="showContactInfo(contact)">
{{ contact.name }}
</div>
</div>
<div v-if="contactInfo" class="np-contact-info">
<div class="no-contact-info--name">
{{ contactInfo.name }}
</div>
<div class="np-contact-info--number">
{{ contactInfo.number }}
</div>
<div class="np-contact-info--email">
{{ contactInfo.email }}
</div>
</div>
</div>
</div>
</template>
<script>
import { contactsList } from "./../data/contacts";
export default {
name: "PhoneView",
data() {
return {
contactInfo: null,
};
},
computed: {
contacts() {
return contactsList;
},
},
methods: {
showContactInfo(contact) {
this.contactInfo = contact;
},
},
};
</script>
<style>
.np-contact-name {
font-size: 12px;
border-bottom: 1px solid #e9e9e9;
padding: 4px 0px;
font-weight: normal;
transition: all 0.3s;
cursor: pointer;
}
.np-contact-name:hover {
font-size: 12px;
font-weight: bold;
padding: 4px 0px;
transition: all 0.3s;
color: #0059ff;
}
.np-contact-info {
font-size: 13px;
border: 1px solid #e9e9e9;
border-radius: 8px;
margin-top: 10px;
padding: 4px 10px;
}
.no-contact-info--name {
font-size: 15px;
padding-bottom: 4px;
}
.np-contact-info--email {
color: #0059ff;
}
</style>
Code language: HTML, XML (xml)
You’d notice that there’s not much going on in this view. It’ll render the details of a contact whenever you hover over a contact name. I’ve done so on line 6 and 41.
NotesView component
Now, let’s look at the NotesView.vue:
<template>
<div>
<div class="np-app-container">
<div class="np-app-title">Notes</div>
<div v-for="(note, n) in notesList" :key="n">
<div class="np-saved-note">
{{ note }}
</div>
</div>
<div class="np-note-editor">
<textarea
placeholder="Type something..."
v-model="noteData"
@keyup.enter="saveNote"
class="np-note-editor-area"
/>
<div @click="saveNote" class="np-save-btn">Save</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "NotesView",
data() {
return {
notesList: [],
noteData: null,
};
},
methods: {
saveNote() {
this.noteData && this.notesList.push(this.noteData);
this.noteData = null;
},
},
};
</script>
<style>
.np-note-editor {
font-size: 14px;
position: absolute;
bottom: 100px;
}
.np-note-editor-area {
width: 200px;
height: 100px;
overflow: auto;
resize: none;
border: 1px solid #0059ff;
border-radius: 4px;
outline: none;
padding: 4px;
font-size: 16px;
}
.np-saved-note {
font-size: 14px;
border-bottom: 1px solid #eee;
padding: 4px 0px;
}
.np-save-btn {
float: right;
margin-right: 25px;
margin-top: 6px;
cursor: pointer;
color: #0059ff;
}
</style>
Code language: HTML, XML (xml)
The functioning for the Notes view is pretty simple. I’ve created a local state notesList which keeps a list of notes. I’ve also created a local state called noteData which keeps the currently entered note in the note editor. Whenever you click on the Save button, the function saveData() on line 33, appends this data to the notesList state.
HomeWidget component
With that done, let’s look at the HomeWidget.vue:
<template>
<div>
<div class="np-clock-current-time np-home-font">
{{ currentTime }}
</div>
<div class="np-author">Gautam Kabiraj</div>
</div>
</template>
<script>
import moment from "moment";
export default {
name: "ClockView",
data() {
return {
currentTime: moment.utc(new Date()).local().format("hh:mm a"),
};
},
};
</script>
<style>
.np-home-font {
color: #fff;
font-size: 40px;
padding-left: 30px;
padding-top: 20px;
}
.np-author {
padding-left: 30px;
color: #fff;
font-size: 12px;
}
</style>
Code language: HTML, XML (xml)
In the HomeWidget view, I’m using a transparent background with the current date and time. I’ve used the popularĀ moment.jsĀ library to convert the local date time and show it in my preferred way (line 16). You can install the library with:
npm install moment --save
GalleryView component
Now, let’s look at the GalleryView:
<template>
<div>
<div class="np-app-container">
<div v-if="hasImagePreview" class="np-img-preview-container">
<img :src="hasImagePreview" class="np-img-preview" />
<div @click="closeImagePreview()" class="np-img-preview--close">
Close
</div>
</div>
<div class="np-app-title">Gallery</div>
<div class="np-gallery-container">
<div v-for="(image, i) in imagesList" :key="i" class="np-img-thumbnail">
<img
@click="setImagePreview(image)"
:src="image"
class="np-img-src-tn"
/>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "GalleryView",
data() {
return {
hasImagePreview: null,
};
},
computed: {
imagesList() {
let imageIds = [];
for (let i = 0; i < 14; i++) {
imageIds.push(Math.floor(Math.random() * 1000) + 1);
}
return imageIds.map((img) => {
return `https://picsum.photos/id/${img}/300/300`;
});
},
},
methods: {
setImagePreview(imageToPreview) {
this.hasImagePreview = imageToPreview;
},
closeImagePreview() {
this.hasImagePreview = null;
},
},
};
</script>
<style>
.np-img-thumbnail {
display: inline-block;
padding-right: 8px;
}
.np-gallery-container {
margin-top: 10px;
margin-left: 4px;
}
.np-img-src-tn {
width: 60px;
height: 60px;
cursor: pointer;
}
.np-img-preview-container {
position: absolute;
left: 0px;
top: 0px;
background: rgba(0, 0, 0, 0.5);
height: 500px;
width: 250px;
}
.np-img-preview {
position: absolute;
left: 18px;
width: 210px;
height: 210px;
top: 90px;
border: 2px solid #000;
border-radius: 6px;
}
.np-img-preview--close {
position: absolute;
color: #fff;
background: #0059ff;
border-radius: 6px;
height: 15px;
width: 40px;
right: 24px;
top: 94px;
z-index: 99;
padding: 4px;
text-align: center;
font-size: 12px;
cursor: pointer;
}
</style>
Code language: HTML, XML (xml)
In this views, I’ve created a computed property imagesList on line 33. Therein, I’ve randomly generated 14 images with ids varying between 1 and 1000. That means, there’s a 0.001% of repetition chance for each of the image. I’ve then iterated over those images on line 12. Whenever you click on an image, I copy the image url to the hasImagePreview local state. I’ve then rendered this preview image on line 5.
ClockView component
Now, let’s look at ClockView:
<template>
<div class="np-app-container">
<div class="np-app-title">Clock</div>
<div class="np-clock-current-time">
{{ currentTime }}
</div>
<div>
{{ getDayOfTheWeek(currentDay) }}
</div>
<div class="np-current-date">
{{ currentDate }}
</div>
<div class="np-credit">www.nightprogrammer.com</div>
<div>
<img class="np-clock-img" src="https://picsum.photos/210/240" />
</div>
</div>
</template>
<script>
import moment from "moment";
export default {
name: "ClockView",
data() {
return {
currentTime: moment.utc(new Date()).local().format("hh:mm:a"),
currentDate: moment.utc(new Date()).local().format("MM/DD/YYYY"),
currentDay: moment.utc(new Date()).local().day(),
};
},
methods: {
getDayOfTheWeek(currentDay) {
switch (currentDay) {
case 1:
return "Monday";
case 2:
return "Tuesday";
case 3:
return "Wednesday";
case 4:
return "Thursday";
case 5:
return "Friday";
case 6:
return "Saturday";
case 7:
return "Sunday";
default:
return "Good day";
}
},
},
};
</script>
<style>
.np-clock-current-time {
font-size: 30px;
margin-top: 10px;
}
.np-current-date {
padding-top: 10px;
}
.np-clock-img {
border-radius: 4px;
margin-top: 15px;
}
.np-credit {
font-size: 12px;
}
</style>
Code language: HTML, XML (xml)
The functionality of the ClockView component is same as the HomeView component. Except that I’ve also shown the day of the week of the current day. I’ve done so on line 32 using getDayOfTheWeek() custom method.
Finally, here’s the code for App.vue which mounts all the components inside it by including the MainWrapper component:
<template>
<div id="app">
<MainWrapper />
</div>
</template>
<script>
import MainWrapper from "./components/MainWrapper";
export default {
name: "App",
components: {
MainWrapper,
},
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.np-app-title {
text-align: center;
width: 200px;
}
.np-app-container {
width: 210px;
height: 395px;
margin: 0 auto;
margin-top: 10px;
background: #fff;
border-radius: 10px;
padding: 12px;
}
</style>
Code language: HTML, XML (xml)
Here are a few samples on how the UI should look like:
You can find a working copy of the above code from my repos here: